简体   繁体   中英

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
    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\Event", mappedBy="device")
     */
    protected $events;


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

The problem comes because Device is a Single Table Inheritance entity

 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(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 :

/**
 * @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 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?

I'd suggest you couple of options to consider here.

1) As per Doctrine's documentation you can use fetch="EAGER" to hint Doctrine that you want the relation eagerly fetched whenever the entity is being loaded:

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

If used carefully this can save you from firing additional queries upon iteration but has it's own drawbacks either.

If you start to extensively use forced eager loading you may find yourself in situation where loading an entity to read a simple attribute from it will result in loading tens and even hundreds of relations. This may not look as bad from SQL point of view (perhaps a single query) but remember that all the results will be hydrated as objects and attached to the Unit Of Work to monitor them for changes.

2) If you are using this for reporting purposes (eg display all events for a device) then it's better not to use entities at all 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'll skip the expensive hydration and monitoring by the UoM as it's unlikely to modify entities in such a case. This is considered a "best practice" too when using Doctrine for reporting.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM