我有一个非常简单的实体(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的所有元素:
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加载中触发并添加已经“手动”添加的相同子项。
问:有没有办法阻止/禁用此行为并告诉这些实体他们与数据库同步,因此不需要进行额外的查询?
答案 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时,我就这样做了。在嵌套集中,您可以使用root
,level
,left
和right
列来限制/扩展已获取的对象。它确实需要一些复杂的子查询,但它是可行的。
嵌套集的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无关,它将生成其他查询。