Doctrine - Hydrate Harvesting in Entity Class

I have a problem with the OneToMany <-> ManyToOne bidirectional relationship between my Device and Event objects. This is how the display looks:

 // Device entity /** * @ORM\OneToMany(targetEntity="AppBundle\Entity\Event", mappedBy="device") */ protected $events; // Event entity /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Device", inversedBy="events") */ protected $device; 

The problem arises because Device is a single table inheritance object.

  * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="device_class_type", type="string") 

and every time I select and iterate over some Event entities, then $device always willingly selected. This is because it is an STI object, as indicated in the relative documentation.

There is a general performance assessment using a single table Inheritance: if the target from a many-to-one or one-to-one association is an STI object, it is preferable for performance reasons that it is a sheet element in the inheritance hierarchy (i.e. there are no subclasses). Otherwise, Doctrine CANNOT create proxy instances of this and will ALWAYS load the object with impatience.

Now there is another object called Gateway , which has a relationship with both Device and Event :

 /** * @ORM\OneToMany(targetEntity="AppBundle\Entity\Device", mappedBy="gateway") */ protected $devices; /** * @ORM\OneToMany(targetEntity="targetEntity="AppBundle\Entity\Event", mappedBy="gateway") */ protected $events; public function getEvents(): Collection { return $this->events; } 

And, of course, every time I $gateway->getEvents() over $gateway->getEvents() , all devices with relative events look impatiently. This happens even if I don't get any $device information - an empty foreach enough for Doctrine to execute 1 request for each object to get the associated $device

 foreach ($gateway->getEvents() as $event) {} 

Now I know that I could use QueryBuilder to set a different hydration mode, avoiding $device fetching

 return $this->getEntityManager()->createQueryBuilder() ->select('e') ->from('AppBundle:Event', 'e') ->where('e.gateway = :gateway') ->setParameter('gateway', $gateway) ->getQuery()->getResult(Query::HYDRATE_SIMPLEOBJECT); 

but I would like to do it somehow directly in the Gateway object.

So, is it possible to hydrate Gateway->events directly in the Gateway entity class?

+7
collections php symfony doctrine2
source share
2 answers

You need to write your own hydration method.

You have a circular link where one of these nodes (Device) will force FETCH EAGER . Worse, one of these nodes (Gateway) acts like ManyToMany, joins a table between two others, resulting in a FETCH EAGER loading everything into an almost infinite loop (or at least large blocks of related data).

  +──< OneToMany >──+ ManyToOne >──< ManyToMany +──+ OneToOne ┌──────< Gateway >──────┐ │ │ + + Event +──────────────< Device* 

As you can see, when a device makes an EAGER selection, it will collect many Gateways , thus many Events , thus many Devices , thus many other Gateways , etc. FETCH EAGER will continue to move until all links are filled.

Prevent hydration "EAGER" by creating your own hydrator.

Building your own hydrator will require careful data processing, but it will probably be somewhat simple for your use. Remember to register the hydrator with Doctrine and pass it as an argument to $query->execute([], 'GatewayHydrator');

 class GatewayHydrator extends DefaultEntityHydrator { public function hydrateResultSet($stmt) { $data = $stmt->fetchAll(PDO::FETCH_ASSOC); $class = $this->em->getClassMetadata(Gateway::class); $gateway = $class->newInstance(); $gateway->setName($data[0]['gateway_name']); // example only return $gateway; } } 

Alternatively, remove the displayed field from the device to the gateway.

Removing the $gateway => Gateway mapping from Device and mappedBy="gateway" from the Gateway->device mapping, the device will effectively become a worksheet in terms of the Doctrine. This will avoid this link cycle with one drawback: the Device-> gateway property must be set manually (possibly in the Gateway and Event setDevice ).

+1
source share

I would advise you to consider several options here.

1) According to the Doctrine Documentation, you can use fetch="EAGER" to hint to Doctrine that you want the relation to be eagerly loaded whenever the object is loaded:

 /** * @ORM\OneToMany(targetEntity="AppBundle\Entity\Device", mappedBy="gateway", fetch="EAGER") */ protected $devices; 

If used carefully, this can save you from having to run additional requests for iteration, but it also has its drawbacks.

If you start to use forced loading intensively, you may find yourself in a situation where loading an object to read a simple attribute from it will lead to the loading of tens or even hundreds of relations. This may not seem so bad from the SQL point of view (perhaps with a single query), but remember that all results will be hydrated as objects and tied to Unit of Work to monitor their changes.

2) If you use this for reporting purposes (for example, displaying all events for a device), it is better not to use entities, but to request array hydration from Doctrine. In this case, you will be able to control what gets into the result by explicitly joining the relation (or not). As an added benefit, you will skip the expensive hydration and monitoring by the UoM, as they are unlikely to alter the facilities in this case. This is considered "best practice" when using Doctrine for reporting.

+1
source share

All Articles