9

自己参照関係(それが呼ばれる隣接リスト)で相互に接続されたメニュー項目を保持する非常に単純なエンティティ(WpmMenu)がありますか?だから私の実体では私は持っています:

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

すべてのゲッター/セッターとの関係は次のとおりです。

/**
* @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のアイテムを使用します同じように)。メニューツリーのすべての要素を取得するには、Doctrineを実行します。

  • ルート要素の1つのクエリ+
  • ルート要素の5つの子(レベル= 1)を取得するための1つのクエリ+
  • レベル1の各アイテムの3つの子(レベル= 2)を取得するための5つのクエリ+
  • 各レベル2アイテムの子(レベル= 3)を取得するための15クエリ(5x3)

合計:22クエリ

したがって、これに対する解決策を見つける必要があります。理想的には、クエリを1つだけにします。

これが私がやろうとしていることです。 エンティティリポジトリ(WpmMenuRepository)で、queryBuilderを使用して、レベル順に並べられたすべてのメニュー項目のフラット配列を取得します。ルート要素(WpmMenu)を取得し、ロードされた要素の配列からその子を「手動で」追加します。次に、これを子供に対して再帰的に実行します。このようにすることで、同じツリーを1つのクエリで作成できます。

だからこれは私が持っているものです:

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

そして、これが起こることです:

すべてがうまくいきますが、私は各メニュー項目が2回存在することになります。;)22のクエリの代わりに、現在23のクエリがあります。したがって、実際にケースを悪化させました。

実際に起こることは、「手動で」追加された子を追加した場合でも、WpmMenuエンティティはデータベースと同期しているとは見なされず、子に対してforeachループを実行するとすぐに、ORMで読み込みがトリガーされることです。すでに「手動で」追加されたものと同じ子をロードして追加します。

Q:この動作をブロック/無効にして、これらのエンティティにデータベースと同期していることを通知する方法はありますか?追加のクエリは必要ありませんか?

4

4 に答える 4

17

非常に安心して(そしてDoctrine HydrationとUnitOfWorkについて多くのことを学んだ)、私はこの質問に対する答えを見つけました。そして、多くのことと同様に、答えを見つけたら、数行のコードでこれを達成できることに気付きます。私はまだこれを未知の副作用についてテストしていますが、正しく機能しているようです。問題が何であるかを特定するのは非常に困難でした。一度実行すると、答えを探すのがはるかに簡単になりました。

したがって、問題は次のとおりです。これは、ツリー全体が要素のフラット配列としてロードされ、setupTreeFromFlatCollectionメソッドによって各要素の$ children配列に「手動で供給」される自己参照エンティティであるため、getChildren ()メソッドはツリー内のエンティティ(ルート要素を含む)のいずれかで呼び出され、Doctrine(この「手動」アプローチについては知らない)は要素を「未初期化」と見なし、SQLを実行して関連するすべての子をフェッチしますデータベースから。

そこで、ObjectHydratorクラス(\ Doctrine \ ORM \ Internal \ Hydration \ ObjectHydrator)を分析し、脱水プロセス(一種)に従って$reflFieldValue->setInitialized(true);、\ Doctrine \ ORM \ PersistentCollectionクラスのメソッドである@line:369に到達しました。クラス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

隣接リストを使用する場合、この問題を解決することはできません。そこに行って、それをしました。唯一の方法は、ネストされたセットを使用することです。そうすれば、1つのクエリで必要なものすべてをフェッチできます。

Doctrine1を使用していたときにそれを行いました。ネストされたセットにはroot、フェッチされたオブジェクトを制限/拡張するために使用できる、、、および列がありlevelますleftrightやや複雑なサブクエリが必要ですが、実行可能です。

ネストされたセットの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 に答える