问题描述
我有一个非常简单的实体(WpmMenu),它包含以自引用关系(称为相邻列表)相互连接的菜单项?所以在我的实体中,我有:
I have a very simple entity(WpmMenu) that holds menu items connected to one another in a self-referencing relationship (adjecent list it's called)?so in my entity I have:
protected $id
protected $parent_id
protected $level
protected $name
所有 getter/setter 的关系是:
with all the getters/setters the relationships are:
/**
* @ORMOneToMany(targetEntity="WpmMenu", mappedBy="parent")
*/
protected $children;
/**
* @ORMManyToOne(targetEntity="WpmMenu", inversedBy="children", fetch="LAZY")
* @ORMJoinColumn(name="parent_id", referencedColumnName="id", onUpdate="CASCADE", onDelete="CASCADE")
*/
protected $parent;
public function __construct() {
$this->children = new ArrayCollection();
}
一切正常.当我渲染菜单树时,我从存储库中获取根元素,获取其子元素,然后遍历每个子元素,获取其子元素并递归执行此操作,直到我渲染了每个项目.
And everything works fine. When I render the menu tree, I get the root element from the repository, get its children, and then loop through each child, get its children and do this recursively until I have rendered each item.
会发生什么(以及我正在寻求的解决方案)是这样的:目前我有 5 个级别 = 1 的项目,并且这些项目中的每一个都附加了 3 个级别 = 2 的项目(将来我也会使用级别 = 3 的项目).获取我的菜单树 Doctrine 的所有元素:
What happens (and for what I am seeking a solution)is this:At the moment I have 5 level=1 items and each of these items have 3 level=2 items attached (and in the future I will be using level=3 items as well). To get all elements of my menu tree Doctrine executes:
- 1 次查询根元素 +
- 1 次查询获取根元素的 5 个子元素(级别 = 1) +
- 5 次查询以获取每个级别 1 项目的 3 个子项(级别 = 2) +
- 15 个查询 (5x3) 以获取每个 2 级项目的子项(级别 = 3)
总计:22 次查询
所以,我需要为此找到一个解决方案,理想情况下我只想有 1 个查询.
So, I need to find a solution for this and ideally I would like to have 1 query only.
所以这就是我想要做的:在我的实体存储库 (WpmMenuRepository) 中,我使用 queryBuilder 并获取按级别排序的所有菜单项的平面数组.获取根元素(WpmMenu)并从加载的元素数组中手动"添加其子元素.然后对儿童递归执行此操作.这样做我可以拥有相同的树,但只需一个查询.
So this is what I am trying to do:In my entities repository(WpmMenuRepository) I use queryBuilder and get a flat array of all menu items ordered by level. Get the root element(WpmMenu) and add "manually" its children from the loaded array of elements. Then do this recursively on children. Doing this way I could have the same tree but with a single query.
这就是我所拥有的:
WpmMenuRepository:
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 实体中,我有:
and in my WpmMenu entity I have:
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);
}
这就是发生的事情:
一切正常,但我最终让每个菜单项出现了两次.;) 现在我有 23 个查询而不是 22 个查询.所以我实际上使情况变得更糟.
Everything works out fine, BUT I end up with each menu items present twice. ;) Instead of 22 queries now I have 23. So I actually worsened the case.
我认为,真正发生的事情是,即使我添加了手动"添加的子项,WpmMenu 实体也不会被视为与数据库同步,一旦我对其子项执行 foreach 循环,加载就是在 ORM 加载中触发并添加已手动"添加的相同子项.
What really happens, I think, is that even if I add the children added "manually", the WpmMenu entity is NOT considered in-sync with the database and as soon as I do the foreach loop on its children the loading is triggered in ORM loading and adding the same children that were added already "manually".
问:有没有办法阻止/禁用此行为并告诉这些实体它们与数据库同步,因此不需要额外的查询?
Q: Is there a way to block/disable this behaviour and tell these entities they they ARE in sync with the db so no additional querying is needed?
推荐答案
带着极大的安慰(以及大量关于 Doctrine Hydration 和 UnitOfWork 的知识),我找到了这个问题的答案.和很多事情一样,一旦你找到答案,你就会意识到你可以用几行代码来实现这一点.我仍在测试这个未知的副作用,但它似乎工作正常.我在确定问题所在时遇到了很多困难 - 一旦我确定了,寻找答案就容易多了.
With immense relief (and a lots of learning about Doctrine Hydration and UnitOfWork) I found the answer to this question. And as with lots of things once you find the answer you realize that you can achieve this with a few lines of code. I am still testing this for unknown side-effects but it seems to be working correctly.I had quite a lot of difficulties to identify what the problem was - once I did it was much easier to search for an answer.
所以问题是这样的:由于这是一个自引用实体,其中整个树作为元素的平面数组加载,然后通过 setupTreeFromFlatCollection 方法手动"将它们馈送到"每个元素的 $children 数组 -当在树中的任何实体(包括根元素)上调用 getChildren() 方法时,Doctrine(不知道这种手动"方法)将元素视为未初始化",因此执行 SQL 以获取所有其相关子项来自数据库.
So the problem is this: Since this is a self-referencing entity where the entire tree is loaded as a flat array of elements and then they are "fed manually" to the $children array of each element by the setupTreeFromFlatCollection method - when the getChildren() method is called on any of the entities in the tree (including the root element), Doctrine (NOT knowing about this 'manual' approach) sees the element as "NOT INITIALIZED" and so executes an SQL to fetch all its related children from the database.
所以我剖析了 ObjectHydrator 类 (DoctrineORMInternalHydrationObjectHydrator) 并遵循(某种程度的)脱水过程,然后我得到了一个 $reflFieldValue->setInitialized(true);
@line:369 这是 DoctrineORMPersistentCollection 类上的一个方法,在类 true/false 上设置 $initialized 属性.所以我试过了,它有效!!!
So I dissected the ObjectHydrator class (DoctrineORMInternalHydrationObjectHydrator) and I followed (sort of) the dehydration process and I got to a $reflFieldValue->setInitialized(true);
@line:369 which is a method on the DoctrineORMPersistentCollection class setting the $initialized property on the class true/false. So I tried and IT WORKS!!!
对 queryBuilder 的 getResult() 方法返回的每个实体执行 ->setInitialized(true)(使用 HYDRATE_OBJECT === ObjectHydrator)然后在实体上调用 ->getChildren() 现在不会触发任何进一步的 SQL !!!
Doing a ->setInitialized(true) on each of the entities returned by the getResult() method of the queryBuilder (using the HYDRATE_OBJECT === ObjectHydrator) and then calling ->getChildren() on the entities now do NOT trigger any further SQLs!!!
把它集成到WpmMenuRepository的代码中,就变成了:
Integrating it in the code of WpmMenuRepository, it becomes:
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 DoctrineORMPersistentCollection
}
/** @var $treeRoot WpmMenu */
$treeRoot = array_pop($res);
$treeRoot->setupTreeFromFlatCollection($res);
return($treeRoot);
}
仅此而已!
这篇关于Doctrine - 自引用实体 - 禁止获取孩子的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!