Skip to content
Advertisement

Doctrine – Hydrate collection in Entity class

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).

User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement