2012-11-29 43 views
7

我有一個非常簡單的實體(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個等級= 1個物品,每個物品都附有3個等級= 2個物品(並且將來我會使用級別= 3項目)。爲了讓我的菜單樹學說中的所有元素執行:

  • 1查詢根元素+
  • 1查詢得到5個孩子的根元素(等級= 1)+
  • 5查詢每個級別的獲得3個孩子每個電平1項+
  • 15查詢(5×3)的(等級= 2),以獲得兒童(等級= 3)2項

總計: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); 
} 

這是發生了什麼:

一切工作正常,但我結束與每個菜單項出現兩次。 ;)而不是22個查詢現在我有23個。所以我實際上惡化了案件。

真的發生了什麼,我認爲,即使我添加了「手動」添加的子項,WpmMenu實體也不會被視爲與數據庫同步,並且只要我對其子項執行foreach循環,在ORM加載中觸發,並添加已經「手動」添加的相同子項。

Q:有沒有辦法阻止/禁止這種行爲,並告訴這些實體他們他們是與數據庫同步所以不需要額外的查詢?

+1

看看這是否有幫助:http://docs.doctrine-project.org/zh/latest/reference/partial-objects.html – gremo

+0

沒有。我沒有部分對象,這似乎是一個壞主意。 – jakabadambalazs

回答

14

隨着巨大的緩解(以及關於學說水合和UnitOfWork的大量學習),我找到了這個問題的答案。就像許多事情一旦你找到答案,你意識到你可以用幾行代碼實現這一點。我仍在測試這個未知的副作用,但它似乎工作正常。 我有很多困難來確定問題所在 - 一旦我找到答案就更容易找到答案。

所以,問題是:由於這是其中整個樹被加載爲元件的平面陣列的自參考實體,然後將它們的「手動饋送」,以各元素的$孩子陣列由setupTreeFromFlatCollection方法 - 當在樹中的任何實體(包括根元素)上調用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); 
} 

而這一切!

+2

我可以堅信,沒有人可以在網絡上的其他地方找到這個精心設計的解決方案。即使在教義文獻中,你也可以找到這種常見情況的提及,例如在呈現嵌套菜單或評論時發生的情況。 謝謝@ jakabadambalazs你做了我的一天。 – sepehr

+0

我面臨完全相同的問題。這裏的問題在於:'當通過主鍵索引對象時,通過主鍵索引的身份映射只允許使用快捷方式。'這意味着當您調用'getChildren()'時,所有這些頁面都會通過非主索引查詢UnitOfWork鍵字段(aka'$ parent'),然後教義再次查詢數據庫...你正在做的是基本上阻止進一步的延遲加載和手動初始化子PersistentCollection ...因爲你只是設置孩子的'$初始化' 'PersistentCollection'我沒有看到任何缺點.. – KnF

+0

這確實是一個hacky的解決方案..但遠比所有嵌套設置的混亂 – KnF

0

將註釋添加到您的關聯中以啓用預先加載。這應該允許您僅用1個查詢加載整個樹,並避免必須從平面陣列重建它。

實施例:

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

註釋是這一個,但與所述值變更 https://doctrine-orm.readthedocs.org/en/latest/tutorials/extra-lazy-associations.html?highlight=fetch

+1

沒有雪茄!當我獲取樹的根元素時,關係上的'fetch =「EAGER」'選項會立即觸發整個樹的加載。但是,像以前那樣,所有其他查詢都是通過'WHERE t0.parent_id =?'執行的。換句話說,唯一的變化就是即使我沒有訪問孩子,查詢也會被執行,但是實際的結構和因此獲取元素所需的查詢是相同的 - 所以結果是一樣的 – jakabadambalazs

0

如果使用相鄰列表可以不解決這個問題。去過也做過。唯一的方法是使用嵌套集,然後你就可以在單個查詢中獲取你需要的所有東西。

我在使用Doctrine1時做到了這一點。在嵌套集中,您可以使用rootlevelleftright列來限制/展開提取的對象。它確實需要一些複雜的子查詢,但它是可行的。

嵌套集的D1文檔非常好,我建議檢查一下,你會更好地理解這個想法。

+0

謝謝,但仍然在黑暗!這是我得到的:1)嵌套集非常酷 - 我花了4小時閱讀它,並且我學到了很多東西,但是在一天結束時我會用QueryBuilder做一個查詢,可以:' - > getResult()'/' - > getArrayResult()'/' - > getScalarResult()'這兩個方法都不會返回我需要的東西。 ' - > getArrayResult()'是最接近的'它返回一個嵌套的數組數組(至少這是它似乎在做什麼,但我仍然需要檢查它) - 但我認爲實際問題不是我怎麼擺脫我的數據在數據庫中,但水化 – jakabadambalazs

+0

事實上,因爲它站在(https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight =水合#水合模式)教義參考書@ 14.7.4 - 水合模式 - 似乎沒有什麼能夠將平面2D數據融合到單個結果集上的樹狀結構。無論是鄰接列表還是嵌套集合。 – jakabadambalazs

+0

讓我檢查我是否正確理解你;你想有一個查詢將獲取整個樹,甚至可能有一些連接,對嗎?如果是這樣,你真的必須使用嵌套集。我可以複製並粘貼我擁有的示例代碼,但是它適用於D1,對於項目非常具體,因此對您來說沒有多大用處。訣竅在於巧妙地使用「左」和「右」列。最簡單的解決方案是創建另一個嵌套集的實體,創建一些樹並檢查數據庫中的值。然後你會知道如何做到這一點。並btw;我津津有味的對象,不需要轉換。 – Zeljko

0

這更像是一個完成和更清潔的解決方案,但是基於接受的答案...

唯一需要的是,將要查詢平樹結構,然後,通過迭代這個數組它將自定義庫,第一標記爲初始化的孩子集合,然後將的addChild二傳手存在於hydratate它父實體..

<?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所關聯,它會生成額外的查詢。