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 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.
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;
}
}
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.