Doctrine - 自引用实体 - 禁用获取子项

jak*_*azs 9 php orm doctrine query-builder symfony

我有一个非常简单的实体(WpmMenu),它以自引用关系(称为adjecent list)保存相互连接的菜单项?所以在我的实体中我有:

protected $id
protected $parent_id
protected $level
protected $name
Run Code Online (Sandbox Code Playgroud)

与所有的getter/setter关系是:

/**
* @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();
}
Run Code Online (Sandbox Code Playgroud)

一切正常.当我渲染菜单树时,我从存储库中获取根元素,获取其子元素,然后循环遍历每个子节点,获取其子节点并递归执行此操作,直到我渲染每个项目.

会发生什么(以及我正在寻求解决方案)是这样的:目前我有5个等级= 1个项目,并且每个项目都有3个等级= 2个项目附加(并且将来我将使用等级= 3个项目以及).要获取菜单树Doctrine的所有元素,请执行:

  • 1查询根元素+
  • 1个查询以获取5个子元素(level = 1)的根元素+
  • 5个查询以获得每个级别1项目的3个孩子(级别= 2)+
  • 15个查询(5x3)以获取每个级别2项的子项(级别= 3)

总计: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);
}
Run Code Online (Sandbox Code Playgroud)

在我的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);
}
Run Code Online (Sandbox Code Playgroud)

这就是发生的事情:

一切都很好,但最终每个菜单项目出现两次.;)而不是22个查询现在我有23个.所以我实际上恶化了这个案子.

我认为,实际发生的事情是,即使我添加了"手动"添加的子项,WpmMenu实体也不会被认为与数据库同步,只要我对其子项执行foreach循环,就会在ORM中触发加载加载和添加已经"手动"添加的相同子项.

:有没有办法阻止/禁用此行为并告诉这些实体他们与数据库同步,因此不需要额外的查询?

jak*_*azs 16

有了巨大的解脱(以及关于Doctrine Hydration和UnitOfWork的大量学习),我找到了这个问题的答案.和许多事情一样,一旦你找到了答案,你就会意识到你可以通过几行代码实现这一目标.我仍在测试这个未知的副作用,但似乎工作正常.我有很多困难来确定问题所在 - 一旦我这样做,就更容易找到答案.

所以问题是这样的:因为这是一个自引用实体,其中整个树作为一个平面元素数组加载,然后通过setupTreeFromFlatCollection方法将它们"手动"馈送到每个元素的$ children数组 - 当getChildren时在树中的任何实体(包括根元素)上调用()方法,Doctrine(不知道这个'手动'方法)将元素视为"NOT INITIALIZED",因此执行SQL以获取其所有相关的子元素来自数据库.

所以我解剖了ObjectHydrator类(\ Doctrine\ORM\Internal\Hydration\ObjectHydrator),然后我跟着(排序)脱水过程,我得到了$reflFieldValue->setInitialized(true);@line:369,这是\ Doctrine\ORM\PersistentCollection类的一个方法在类true/false上设置$ initialized属性.所以我尝试了,它的工作原理!

在queryBuilder的getResult()方法返回的每个实体上执行 - > setInitialized(true)(使用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);
}
Run Code Online (Sandbox Code Playgroud)

就这样!

  • 我可以坚定地说,没有人能够在网络上的任何其他地方找到这个精心设计的解决方案.甚至在教条文档中你都可以找到这种常见情况的提及,例如在渲染嵌套的menues或注释时会发生很多情况.谢谢你@jakabadambalazs你做了我的一天. (2认同)