9

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

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 个 level=1 项目,每个项目都附加了 3 个 level=2 项目(将来我将使用 level=3 项目以及)。要获取我的菜单树 Doctrine 的所有元素,请执行以下操作:

  • 1 查询根元素 +
  • 1 次查询以获取根元素的 5 个子项(级别 = 1)+
  • 5 个查询以获取每个级别 1 项目的 3 个子项(级别 = 2)+
  • 15 个查询 (5x3) 以获取每个级别 2 项目的子项 (level=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);
}

在我的 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);
}

这就是发生的事情:

一切正常,但我最终每个菜单项都出现了两次。;) 现在我有 23 个查询,而不是 22 个查询。所以我实际上使情况恶化了。

我认为,真正发生的情况是,即使我添加了“手动”添加的子项,WpmMenu 实体也不会被视为与数据库同步,并且一旦我对其子项执行 foreach 循环,加载就会在 ORM 中触发加载和添加已经“手动”添加的相同子项。

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

4

4 回答 4

17

带着极大的解脱(以及大量关于 Doctrine Hydration 和 UnitOfWork 的学习),我找到了这个问题的答案。和很多事情一样,一旦你找到答案,你就会意识到你可以用几行代码来实现这一点。我仍在测试它是否有未知的副作用,但它似乎工作正常。我在确定问题所在时遇到了很多困难——一旦我这样做了,寻找答案就容易多了。

所以问题是这样的:由于这是一个自引用实体,其中整个树作为元素的平面数组加载,然后通过 setupTreeFromFlatCollection 方法将它们“手动输入”到每个元素的 $children 数组 - 当 getChildren () 方法在树中的任何实体(包括根元素)上调用,Doctrine(不知道这种“手动”方法)将元素视为“未初始化”,因此执行 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);
}

就这样!

于 2012-12-05T14:55:02.410 回答
0

将注释添加到您的关联中以启用即时加载。这应该允许您仅使用 1 个查询加载整个树,并避免必须从平面数组中重建它。

例子:

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

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

于 2012-11-29T16:38:20.683 回答
0

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

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

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

于 2012-11-30T17:22:03.010 回答
0

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

唯一需要的是一个自定义存储库,它将查询平面树结构,然后,通过迭代这个数组,它首先将子集合标记为已初始化,然后使用父实体中存在的 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;
    }
}

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

于 2016-04-14T11:52:24.433 回答