I have a problem regarding a bi-directional OneToMany <-> ManyToOne
relationship between my entities Device
and Event
. This is how mapping looks:
// Device entity /** * @ORMOneToMany(targetEntity="AppBundleEntityEvent", mappedBy="device") */ protected $events; // Event entity /** * @ORMManyToOne(targetEntity="AppBundleEntityDevice", inversedBy="events") */ protected $device;
The problem comes because Device
is a Single Table Inheritance entity
* @ORMInheritanceType("SINGLE_TABLE") * @ORMDiscriminatorColumn(name="device_class_type", type="string")
and each time I fetch and iterate over some Event
entities, then $device
is always eagerly fetched. This happens because it’s a STI entity as reported in related documentation
There is a general performance consideration with Single Table Inheritance: If the target-entity of a many-to-one or one-to-one association is an STI entity, it is preferable for performance reasons that it be a leaf entity in the inheritance hierarchy, (ie. have no subclasses). Otherwise Doctrine CANNOT create proxy instances of this entity and will ALWAYS load the entity eagerly.
Now there’s another entity called Gateway
which has relationships with both Device
and Event
:
/** * @ORMOneToMany(targetEntity="AppBundleEntityDevice", mappedBy="gateway") */ protected $devices; /** * @ORMOneToMany(targetEntity="targetEntity="AppBundleEntityEvent", mappedBy="gateway") */ protected $events; public function getEvents(): Collection { return $this->events; }
And of course each time I iterate over $gateway->getEvents()
all related events devices are fetched eagerly. This happens even if I don’t get any $device
info – an empty foreach
is enough to let Doctrine execute 1 query for each object to fetch related $device
foreach ($gateway->getEvents() as $event) {}
Now I know 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 Gateway
entity.
So is it possible hydrate Gateway->events
directly in Gateway
entity class?
Advertisement
Answer
You’ll need to write your own hydration method
You have a cyclical reference where one of those nodes (Device) will force a FETCH EAGER
. Making matters worse, one of those nodes (Gateway) is acting like ManyToMany join table between the other two, resulting in FETCH EAGER
loading everything in an near-infinite loop (or at least large blocks of related data).
+──< OneToMany >──+ ManyToOne >──< ManyToMany +──+ OneToOne ┌──────< Gateway >──────┐ │ │ + + Event +──────────────< Device*
As you can see, when device does an EAGER fetch, it will collect many Gateways
, thus many Events
, thus many Devices
, thus many more Gateways
, etc. Fetch EAGER
will keep going until all references are populated.
Prevent “EAGER” Hydration, by building your own Hydrator.
Building your own hydrator will require some careful data-manipulation, but will likely be somewhat simple for your use case. Remember to register your 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 Mapped Field from Device to Gateway
Removing the $gateway => Gateway
mapping from Device
, and mappedBy="gateway"
from the Gateway->device
mapping, Device would effectively become a leaf from Doctrine’s perspective. This would avoid that reference loop, with one drawback: the Device->gateway property would have to be manually set (perhaps in the Gateway and Event setDevice
methods).