У меня очень простая сущность (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:
ИТОГО: 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, оно будет генерировать дополнительные запросы.