Учение – субъект саморегуляции – отключить выборку детей

У меня очень простая сущность (WpmMenu), которая содержит элементы меню, связанные друг с другом в отношении самореференции (список вызываемых имен, который он вызвал)? поэтому в моей сущности у меня есть:

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

со всеми геттерами / сеттерами отношения:

 /** * @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(); } 

И все работает нормально. Когда я визуализую дерево меню, я получаю корневой элемент из репозитория, получаю его дочерние элементы, а затем прохожу через каждый дочерний элемент, получаю его дочерние элементы и делаю это рекурсивно, пока не обработаю каждый элемент.

Что происходит (и для чего я ищу решение) заключается в следующем: на данный момент у меня есть 5 уровней = 1 элемент, и каждый из этих предметов имеет 3 уровня = 2 предмета (и в будущем я буду использовать level = 3 items также). Чтобы получить все элементы моего дерева меню Doctrine:

  • 1 для корневого элемента +
  • 1 для получения 5 детей (уровень = 1) корневого элемента +
  • 5 запросов для получения 3-х детей (уровень = 2) каждого из предметов уровня 1 +
  • 15 запросов (5×3), чтобы получить детей (уровень = 3) каждого уровня 2 предмета

ИТОГО: 22 запроса

Итак, мне нужно найти решение для этого, и в идеале я хотел бы иметь только 1 запрос.

Так вот что я пытаюсь сделать: в моем репозитории объектов (WpmMenuRepository) я использую queryBuilder и получаю плоский массив всех пунктов меню, упорядоченных по уровню. Получите корневой элемент (WpmMenu) и добавьте «вручную» его дочерние элементы из загруженного массива элементов. Затем сделайте это рекурсивно на детей. Для этого я мог бы иметь одно и то же дерево, но с одним запросом.

Так вот что я имею:

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

и в моей организации WpmMenu у меня есть:

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

И вот что происходит:

Все отлично работает, НО я получаю два пункта меню. 😉 Вместо 22 запросов теперь у меня есть 23. Поэтому я действительно ухудшил это дело.

Что действительно происходит, я думаю, это то, что, даже если я добавлю, что дети добавлены «вручную», объект WpmMenu НЕ рассматривается в синхронизации с базой данных, и как только я делаю цикл foreach на своих дочерних элементах, загрузка запускается в ORM загрузка и добавление тех же детей, которые были добавлены уже «вручную».

В : Есть ли способ заблокировать / отключить это поведение и сообщить этим сущностям, что они синхронизируются с db, поэтому не требуется дополнительный запрос?

С огромным облегчением (и большим количеством информации об Учение о докторе и UnitOfWork) я нашел ответ на этот вопрос. И как с множеством вещей, когда вы найдете ответ, вы понимаете, что вы можете добиться этого несколькими строками кода. Я все еще проверяю это на неизвестные побочные эффекты, но, похоже, работает правильно. У меня было довольно много трудностей, чтобы определить, в чем проблема – как только я сделал это, было намного легче найти ответ.

Таким образом, проблема заключается в следующем: поскольку это объект саморегуляции, где все дерево загружается как плоский массив элементов, а затем их «кормят вручную» в массив $ children каждого элемента методом setupTreeFromFlatCollection – когда getChildren () вызывается для любого из объектов в дереве (включая корневой элемент), Doctrine (не зная об этом «ручном» подходе) видит элемент как «НЕ ИНИЦИАЛИЗИРОВАН» и поэтому выполняет SQL для извлечения всех связанных с ним дочерних элементов из базы данных.

Поэтому я расчленял класс ObjectHydrator (\ Doctrine \ ORM \ Internal \ Hydration \ ObjectHydrator), и я последовал (вроде) процесс обезвоживания, и я получил значение $reflFieldValue->setInitialized(true); @line: 369, который является методом класса \ Doctrine \ ORM \ PersistentCollection, устанавливающим свойство $ initialized в классе true / false. Поэтому я пытался и ЭТО РАБОТАЕТ !!!

Выполнение a -> setInitialized (true) для каждого из объектов, возвращаемых методом getResult () queryBuilder (с использованием HYDRATE_OBJECT === ObjectHydrator), а затем вызов -> getChildren () для сущностей теперь НЕ запускает дальнейшие SQL-запросы !!!

Интегрируя его в код WpmMenuRepository, он становится:

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

И это все!

Добавьте аннотацию в свою ассоциацию, чтобы обеспечить высокую загрузку. Это должно позволить вам загружать все дерево только с одним запросом и не восстанавливать его из плоского массива.

Пример:

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

Аннотирование – это одно, но с измененным значением https://doctrine-orm.readthedocs.org/en/latest/tutorials/extra-lazy-associations.html?highlight=fetch

Вы не можете решить эту проблему, если используете соседний список. Был там, сделал это. Единственный способ – использовать вложенный набор, и тогда вы сможете получить все необходимое в одном запросе.

Я сделал это, когда использовал Doctrine1. В вложенном наборе у вас есть столбцы root , level , left и right которые вы можете использовать для ограничения / расширения извлеченных объектов. Это требует нескольких сложных подзапросов, но это выполнимо.

Документация D1 для вложенных наборов довольно хороша, я предлагаю проверить ее, и вы поймете идею лучше.

Это больше похоже на завершение и более чистое решение, но основано на принятом ответе …

Единственное, что нужно – это настраиваемый репозиторий, который будет запрашивать структуру плоской древовидной структуры, а затем, итерируя этот массив, он сначала отметит коллекцию детей как инициализированную, а затем будет гидратировать ее с помощью setter addChild, присутствующего в родительском объекте. ,

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

edit: метод getParent () не будет вызывать дополнительные запросы до тех пор, пока будет произведена связь с первичным ключом, в моем случае атрибут $ parent является прямым отношением к ПК, поэтому UnitOfWork вернет кэшированный объект, а не запрашивать базу данных. Если ваше свойство не связано с PK, оно будет генерировать дополнительные запросы.