Doctrine – entidad autorreferencial – deshabilitar la búsqueda de niños

Tengo una entidad muy simple (WpmMenu) que mantiene los elementos del menú conectados entre sí en una relación autorreferencial (se llama a la lista adjecent)? entonces en mi entidad tengo:

protected $id protected $parent_id protected $level protected $name 

con todos los getters / setters las relaciones son:

 /** * @ORM\OneToMany(targetEntity="WpmMenu", mappedBy="parent") */ protected $children; /** * @ORM\ManyToOne(targetEntity="WpmMenu", inversedBy="children", fetch="LAZY") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onUpdate="CASCADE", onDelete="CASCADE") */ protected $parent; public function __construct() { $this->children = new ArrayCollection(); } 

Y todo funciona bien Cuando renderizo el árbol del menú, obtengo el elemento raíz del repository, obtengo sus hijos, y luego recorro cada niño, obtengo sus hijos y hago esto de forma recursiva hasta que haya procesado cada elemento.

Lo que sucede (y para lo que estoy buscando una solución) es esto: en este momento tengo 5 niveles = 1 ítems y cada uno de estos ítems tiene 3 niveles = 2 ítems adjuntos (y en el futuro usaré nivel = 3 ítems también). Para obtener todos los elementos de mi árbol de menús, Doctrine se ejecuta:

  • 1 consulta para el elemento raíz +
  • 1 consulta para obtener los 5 hijos (nivel = 1) del elemento raíz +
  • 5 consultas para obtener los 3 hijos (nivel = 2) de cada uno de los elementos de nivel 1 +
  • 15 consultas (5×3) para obtener los niños (nivel = 3) de cada nivel 2 elementos

TOTAL: 22 consultas

Entonces, necesito encontrar una solución para esto y, idealmente, me gustaría tener solo 1 consulta.

Así que esto es lo que trato de hacer: en mi repository de entidades (WpmMenuRepository) utilizo queryBuilder y obtengo una matriz plana de todos los elementos del menú ordenados por nivel. Obtenga el elemento raíz (WpmMenu) y agregue “manualmente” sus elementos secundarios desde la matriz cargada de elementos. Luego haz esto recursivamente en los niños. De esta manera podría tener el mismo árbol pero con una sola consulta.

Entonces esto es lo que tengo:

WpmMenuRepository:

 public function setupTree() { $qb = $this->createQueryBuilder("res"); /** @var Array */ $res = $qb->select("res")->orderBy('res.level', 'DESC')->addOrderBy('res.name','DESC')->getQuery()->getResult(); /** @var WpmMenu */ $treeRoot = array_pop($res); $treeRoot->setupTreeFromFlatCollection($res); return($treeRoot); } 

y en mi entidad WpmMenu tengo:

 function setupTreeFromFlatCollection(Array $flattenedDoctrineCollection){ //ADDING IMMEDIATE CHILDREN for ($i=count($flattenedDoctrineCollection)-1 ; $i>=0; $i--) { /** @var WpmMenu */ $docRec = $flattenedDoctrineCollection[$i]; if (($docRec->getLevel()-1) == $this->getLevel()) { if ($docRec->getParentId() == $this->getId()) { $docRec->setParent($this); $this->addChild($docRec); array_splice($flattenedDoctrineCollection, $i, 1); } } } //CALLING CHILDREN RECURSIVELY TO ADD REST foreach ($this->children as &$child) { if ($child->getLevel() > 0) { if (count($flattenedDoctrineCollection) > 0) { $flattenedDoctrineCollection = $child->setupTreeFromFlatCollection($flattenedDoctrineCollection); } else { break; } } } return($flattenedDoctrineCollection); } 

Y esto es lo que pasa:

Todo sale bien, PERO termino con cada elemento del menú presente dos veces. 😉 En lugar de 22 consultas ahora tengo 23. Así que en realidad empeoré el caso.

Lo que realmente sucede, creo, es que incluso si agrego los elementos añadidos “manualmente”, la entidad WpmMenu NO se considera sincronizada con la base de datos y tan pronto como hago el ciclo foreach en sus elementos secundarios, la carga se desencadena en ORM cargando y agregando los mismos elementos secundarios que ya se agregaron “manualmente”.

P : ¿Hay alguna manera de bloquear / deshabilitar este comportamiento y decirles a estas entidades que están en sincronización con el DB, por lo que no es necesario realizar consultas adicionales?

Con inmenso alivio (y aprendí mucho sobre Doctrine Hydration y UnitOfWork) encontré la respuesta a esta pregunta. Y como sucede con muchas cosas una vez que encuentras la respuesta, te das cuenta de que puedes lograrlo con unas pocas líneas de código. Todavía estoy probando esto para detectar efectos secundarios desconocidos, pero parece estar funcionando correctamente. Tuve bastantes dificultades para identificar cuál era el problema; una vez que lo hice, fue mucho más fácil buscar una respuesta.

Entonces, el problema es este: dado que se trata de una entidad autorreferencial donde todo el árbol se carga como una matriz plana de elementos y luego se “alimentan manualmente” a la matriz $ children de cada elemento mediante el método setupTreeFromFlatCollection – cuando getChildren () se invoca a cualquiera de las entidades del árbol (incluido el elemento raíz), Doctrine (SIN conocer este enfoque “manual”) ve el elemento como “NO INICIALIZADO” y, por lo tanto, ejecuta un SQL para buscar todos sus elementos relacionados de la base de datos.

Así que analicé la clase ObjectHydrator (\ Doctrine \ ORM \ Internal \ Hydration \ ObjectHydrator) y seguí (más o menos) el proceso de deshidratación y llegué a $reflFieldValue->setInitialized(true); @line: 369 que es un método en la clase \ Doctrine \ ORM \ PersistentCollection que establece la propiedad $ initialized en la clase verdadero / falso. ¡Así que lo intenté y FUNCIONA!

Hacer un -> setInitialized (true) en cada una de las entidades devueltas por el método getResult () del queryBuilder (utilizando el HYDRATE_OBJECT === ObjectHydrator) y luego llamar a -> getChildren () en las entidades ahora NO desencadenar ningún otro SQL !!!

Al integrarlo en el código de WpmMenuRepository, se convierte en:

 public function setupTree() { $qb = $this->createQueryBuilder("res"); /** @var $res Array */ $res = $qb->select("res")->orderBy('res.level', 'DESC')->addOrderBy('res.name','DESC')->getQuery()->getResult(); /** @var $prop ReflectionProperty */ $prop = $this->getClassMetadata()->reflFields["children"]; foreach($res as &$entity) { $prop->getValue($entity)->setInitialized(true);//getValue will return a \Doctrine\ORM\PersistentCollection } /** @var $treeRoot WpmMenu */ $treeRoot = array_pop($res); $treeRoot->setupTreeFromFlatCollection($res); return($treeRoot); } 

¡Y eso es todo!

Agregue la anotación a su asociación para habilitar la carga ansiosa. Esto debería permitirle cargar todo el árbol con solo 1 consulta y evitar tener que reconstruirlo desde una matriz plana.

Ejemplo:

 /** * @ManyToMany(targetEntity="User", mappedBy="groups", fetch="EAGER") */ 

La anotación es esta pero con el valor cambiado https://doctrine-orm.readthedocs.org/en/latest/tutorials/extra-lazy-associations.html?highlight=fetch

No puede resolver este problema si usa la lista adyacente. Estado allí, hecho eso. La única forma es usar el conjunto nested y luego podrá obtener todo lo que necesita en una sola consulta.

Lo hice cuando estaba usando Doctrine1. En el conjunto nested, tiene columnas de root , level , left y right que puede usar para limitar / expandir los objetos captados. Se requieren subconsultas algo complejas, pero es factible.

La documentación D1 para el conjunto nested es bastante buena, sugiero verificarla y comprenderá mejor la idea.

Esto es más como una solución completa y más limpia, pero se basa en la respuesta aceptada …

Lo único que se necesita es un repository personalizado que consulte la estructura plana del árbol y, al iterar esta matriz, primero marque la colección secundaria como inicializada y luego la hidráteará con el setChild setter presente en la entidad padre. .

 < ?php namespace Domain\Repositories; use Doctrine\ORM\EntityRepository; class PageRepository extends EntityRepository { public function getPageHierachyBySiteId($siteId) { $roots = []; $flatStructure = $this->_em->createQuery('SELECT p FROM Domain\Page p WHERE p.site = :id ORDER BY p.order')->setParameter('id', $siteId)->getResult(); $prop = $this->getClassMetadata()->reflFields['children']; foreach($flatStructure as &$entity) { $prop->getValue($entity)->setInitialized(true); //getValue will return a \Doctrine\ORM\PersistentCollection if ($entity->getParent() != null) { $entity->getParent()->addChild($entity); } else { $roots[] = $entity; } } return $roots; } } 

editar: el método getParent () no desencadenará consultas adicionales siempre que la relación se establezca con la clave principal, en mi caso, el atributo $ parent es una relación directa con el PK, por lo que UnitOfWork devolverá la entidad en caché y no consultar la base de datos … Si su propiedad no está relacionada por PK, generará consultas adicionales.