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

时间:2012-11-29 10:06:47

标签: php symfony orm doctrine query-builder

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

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

所有的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();
}

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

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

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

TOTAL:22次查询

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

所以这就是我要做的事情: 在我的实体存储库(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加载中触发并添加已经“手动”添加的相同子项。

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

4 个答案:

答案 0 :(得分: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()方法返回的每个实体(使用HYDRATE_OBJECT === ObjectHydrator)执行 - > setInitialized(true),然后在实体上调用 - > 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);
}

就是这样!

答案 1 :(得分:0)

将注释添加到关联以启用预先加载。这应该允许您仅使用1个查询加载整个树,并且避免必须从平面阵列重建它。

示例:

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

注释是这个,但值已更改 https://doctrine-orm.readthedocs.org/en/latest/tutorials/extra-lazy-associations.html?highlight=fetch

答案 2 :(得分:0)

如果使用相邻列表,则无法解决此问题。去过也做过。唯一的方法是使用嵌套集,然后您就可以在一个查询中获取所需的所有内容。

当我使用Doctrine1时,我就这样做了。在嵌套集中,您可以使用rootlevelleftright列来限制/扩展已获取的对象。它确实需要一些复杂的子查询,但它是可行的。

嵌套集的D1文档非常好,我建议检查它,你会更好地理解这个想法。

答案 3 :(得分:0)

这更像是一个完成和更清洁的解决方案,但是基于已接受的答案......

唯一需要的是一个自定义存储库,它将查询平面树结构,然后,通过迭代此数组,它将首先将子集合标记为已初始化,然后将使用存在于其中的addChild setter对其进行水化处理。父实体..

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

编辑:只要与主键建立关系,getParent()方法就不会触发其他查询,在我的例子中,$ parent属性是与PK的直接关系,因此UnitOfWork将返回缓存实体而不是查询数据库。如果您的属性与PK无关,它将生成其他查询。

相关问题