教义 - 自我引用实体 - 禁止提取儿童 [英] Doctrine - self-referencing entity - disable fetching of children

查看:98
本文介绍了教义 - 自我引用实体 - 禁止提取儿童的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个非常简单的实体(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 / setters的关系是: p>

with all the getters/setters the relationships are:

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

一切都正常。当我呈现菜单树时,我从存储库获取根元素,获取其子元素,然后循环遍历每个子元素,获取子元素并递归执行,直到渲染每个项目为止。

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个项目(并且在未来我将使用level = 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个子元素(level = 1)的根元素+

  • 5个查询以获得每个1级项目的3个孩子(level = 2)+

  • 15个查询(5x3)获取每个级别的子项(级别= 3)2项

  • 1 query for the root element +
  • 1 query to get the 5 children(level=1) of the root element +
  • 5 queries to get the 3 children(level=2) of each of the level 1 items +
  • 15 queries (5x3) to get the children(level=3) of each level 2 items

TOTAL:22个查询

TOTAL: 22 queries

所以,我需要找到一个解决方案,理想情况下,我只想有一个查询。

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

发生这种情况:

一切都很好,但我最终会出现两个菜单项。而不是现在22查询我已经23了,所以我实际上更糟糕了。

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 :有没有办法阻止/禁用此行为,并告诉这些实体他们与数据库同步,所以不需要额外的查询?

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(不知道此手动方法)将该元素视为NOT INITIALIZED,因此执行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类(\Doctrine\ORM\Internal\Hydration\ObjectHydrator),然后我按照(排序)脱水过程,我得到一个 $ reflFieldValue-> setInitialized(true); @line:369这是\Doctrine\ORM\PersistentCollection c lass设置类为true / false的$ initialized属性。所以我试过,并且它在工作中。

So I dissected the ObjectHydrator class (\Doctrine\ORM\Internal\Hydration\ObjectHydrator) and I followed (sort of) the dehydration process and I got to a $reflFieldValue->setInitialized(true); @line:369 which is a method on the \Doctrine\ORM\PersistentCollection 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 \Doctrine\ORM\PersistentCollection
  }
  /** @var $treeRoot WpmMenu */
  $treeRoot = array_pop($res);
  $treeRoot->setupTreeFromFlatCollection($res);
  return($treeRoot);
}

这就是全部!

这篇关于教义 - 自我引用实体 - 禁止提取儿童的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆