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的所有元素,请执行:
总计: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)
就这样!