2016-02-15 11 views
11

ProductエンティティとReviewエンティティが製品に添付されているとします。 SQLクエリによって返された結果に基づいて、Productエンティティにフィールドを割り当てることは可能ですか? ReviewsCountフィールドをCOUNT(Reviews.ID) as ReviewsCountと同じように付けてください。SQL関数の結果をDoctrineのフィールドとして使用できますか?

私は

public function getReviewsCount() { 
    return count($this->Reviews); 
} 

などの機能にしかし、私は通常、私は数百人をロードする必要はないかもしれないとして、データベースクエリの数を最小限に抑え、パフォーマンスを向上させるためにSQLでこれをやって欲しいことを行うことが可能です知っていますレビューは、まだ数があることを知る必要があります。私はSQLの実行COUNTは、100の製品を通過し、それぞれの100のレビューを計算するよりもはるかに速いと思います。さらに、これはほんの一例です。実際には、より複雑な関数が必要です。MySQLがより高速に処理できると思うのです。私が間違っているなら私を訂正してください。 2.1+あなたが教義にしている場合は

foreach ($result as $row) { 
    echo $row['countResult']; 
    echo $row['anyOtherProductField']; 
} 
+0

私はそうは思わない。名前付きクエリの使用を検討しましたか?[example](http://stackoverflow.com/questions/12016378/doctrine2-named-queries)? –

+0

はい。私はTomasz Madeyskiの答えに対する私のコメントでネイティブクエリーの問題に気づいた。問題は、スカラーをエンティティと一緒に返すことですが、私はそれらをエンティティのフィールドにして混乱を避けたいと考えています。 – Multis

答えて

1

はい、それは可能です、あなたはそれを達成するためにQueryBuilderを使用する必要があります。

$result = $em->getRepository('AppBundle:Product') 
    ->createQueryBuilder('p') 
    ->select('p, count(r.id) as countResult') 
    ->leftJoin('p.Review', 'r') 
    ->groupBy('r.id') 
    ->getQuery() 
    ->getArrayResult(); 

を、今、あなたのような何かを行うことができます使用を検討するEXTRA_LAZY associations

エンティティであなたのようなメソッドを実装したり、すべてのentiを取得する代わりに関連付けを直接行うことができますその中にネクタイ:

/** 
* @ORM\OneToMany(targetEntity="Review", mappedBy="Product" fetch="EXTRA_LAZY") 
*/ 
private $Reviews; 

public function getReviewsCount() { 
    return $this->Reviews->count(); 
} 
+2

問題は、その場合、エンティティとは別のスカラーが得られることです。これらのすべてのスカラーが同じエンティティにアタッチされている場合は問題はありませんが、別のエンティティにアタッチする必要がある場合はどうなりますか?商品やレビューを選択したいのと同じように、商品の場合はレビューの数を、レビューの場合は好き嫌いがたくさんあります。 – Multis

1

、:

+0

良い見えますが、まだ私が必要なものではありません。私が言及したように、実際の計算は少し複雑です。私は正のレビューと否定的なレビューの数を知る必要がありますが、それはすべてのコレクションを読み込んでそれを通過することは不可能と思われます。 – Multis

3

あなたがマップすることができsingle column result to an entity field - これを達成するためにをResultSetMapping native queriesを見てください。簡単な例として:

use Doctrine\ORM\Query\ResultSetMapping; 

$sql = ' 
    SELECT p.*, COUNT(r.id) 
    FROM products p 
    LEFT JOIN reviews r ON p.id = r.product_id 
'; 

$rsm = new ResultSetMapping; 
$rsm->addEntityResult('AppBundle\Entity\Product', 'p'); 
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount'); 

$query = $this->getEntityManager()->createNativeQuery($sql, $rsm); 
$results = $query->getResult(); 

その後、あなたの製品のエンティティであなたは$reviewsCountフィールドを持っているでしょうし、カウントがそれにマッピングされます。あなたがそうのように、Doctrineのメタデータに定義された列を持っている場合にのみ動作することに注意してください:

/** 
* @ORM\Column(type="integer") 
*/ 
private $reviewsCount; 

public function getReviewsCount() 
{ 
    return $this->reviewsCount; 
} 

これはAggregate Fields Doctrineのドキュメントによって提案されたものです。ここで問題となるのは、本質的にDoctrineに、データベースにreviews_countという別の列があると思っているということです。これはあなたが望まないものです。したがって、この列は物理的に追加しなくても機能しますが、doctrine:schema:updateを実行すると、その列が追加されます。残念ながら、Doctrineは実際に仮想プロパティを許可していないので、独自のカスタムハイドレーターを作成するか、またはloadClassMetadataイベントにサブスクライブし、特定のエンティティ(またはエンティティ)がロードされた後に手動でマッピングを手動で追加することもできます。あなたがCOUNT(r.id) AS reviewsCountような何かを行うならば、あなたはもはやあなたのaddFieldResult()機能でCOUNT(id)を使用することはできませんし、代わりにその二番目のパラメータのエイリアスreviewsCountを使用しなければならないこと

は注意してください。

ResultSetMappingBuilderを使用して、結果セットマッピングを使用することもできます。

私の実際の提案は、その余分なものすべてを通過する代わりに手動でこれを行うことです。基本的には、エンティティとスカラーの結果の両方を配列に返す通常のクエリを作成し、そのスカラー結果をエンティティのマッピングされていない対応するフィールドに設定し、エンティティを返します。

+0

これは理想的な解決策ですが、カラムにリンクされていないプロパティに結果を代入すると「未定義インデックス:レビューカウント」例外が発生します。その財産が存在し、公開されている場合でも。それをデータベース列に割り当てずにDoctrineがプロパティにアクセスできるようにする方法はありますか? – Multis

+0

ケースが一致する必要があるので、$ ReviewsCountの代わりに$ reviewsCountでなければなりません。 –

+0

私はそれを理解しています。それは一致します。それにもかかわらず、(たとえ私がdoctrine:schema:updateを行っていなくても)データベースのColumnをそのフィールドに割り当てた場合にのみ動作します。しかし、その場合でも、私は別のクエリで同じエンティティを取得する - 私はこれらの結果を別々に保つが、このフィールドを変更する変更を上書きします。これはdoctrineのキャッシュ結果のように見え、同じエンティティを返しますが、フィールドは再ロードされ、reviewsCountフィールドはリセットされます。 – Multis

2

詳細な調査の後、他の回答に記載されているものに近いものをいくつか見つけましたが、それらのすべてにはいくつかの欠点があります。最後にCustomHydratorsを使用することに決めました。 ORMで管理されていないプロパティは、フィールドとしてResultSetMappingでマッピングすることはできませんが、スカラーとして取得してエンティティに手動で追加することができます。しかし、あなたが教義から得た結果はキャッシュに残っています。つまり、これらのエンティティも含む他のクエリを作成すると、そのように設定されたプロパティがリセットされる可能性があります。

これを行う別の方法は、これらのフィールドをdoctrineのメタデータキャッシュに直接追加することでした。私はCustomHydratorでこれをやってみました:しかし、その方法はエンティティのプロパティのリセットにも問題がありました。だから、私の最終的な解決策は、同じ構造を得るためにはstdClassを使用するだけだったが、教義によって管理されていない:あなたはどのような構造を取得し、あなたが好きしかし、任意のエンティティを添付することができ、そのクラスで

namespace SomeBundle; 

use PDO; 
use Doctrine\ORM\Query\ResultSetMapping; 

class CustomHydrator extends \Doctrine\ORM\Internal\Hydration\ObjectHydrator { 
    public function hydrateAll($stmt, $resultSetMapping, array $hints = array()) { 
     $data = $stmt->fetchAll(PDO::FETCH_ASSOC); 

     $result = []; 

     foreach($resultSetMapping->entityMappings as $root => $something) { 
      $rootIDField = $this->getIDFieldName($root, $resultSetMapping); 

      foreach($data as $row) { 
       $key = $this->findEntityByID($result, $row[$rootIDField]); 

       if ($key === null) { 
        $result[] = new \stdClass(); 
        end($result); 
        $key = key($result); 
       } 

       foreach ($row as $column => $field) 
        if (isset($resultSetMapping->columnOwnerMap[$column])) 
         $this->attach($result[$key], $field, $this->getPath($root, $resultSetMapping, $column)); 
      } 
     } 


     return $result; 
    } 

    private function getIDFieldName($entityAlias, ResultSetMapping $rsm) { 
     foreach ($rsm->fieldMappings as $key => $field) 
      if ($field === 'ID' && $rsm->columnOwnerMap[$key] === $entityAlias) return $key; 

      return null; 
    } 

    private function findEntityByID($array, $ID) { 
     foreach($array as $index => $entity) 
      if (isset($entity->ID) && $entity->ID === $ID) return $index; 

     return null; 
    } 

    private function getPath($root, ResultSetMapping $rsm, $column) { 
     $path = [$rsm->fieldMappings[$column]]; 
     if ($rsm->columnOwnerMap[$column] !== $root) 
      array_splice($path, 0, 0, $this->getParent($root, $rsm, $rsm->columnOwnerMap[$column])); 

     return $path; 
    } 

    private function getParent($root, ResultSetMapping $rsm, $entityAlias) { 
     $path = []; 
     if (isset($rsm->parentAliasMap[$entityAlias])) { 
      $path[] = $rsm->relationMap[$entityAlias]; 
      array_splice($path, 0, 0, $this->getParent($root, $rsm, array_search($rsm->parentAliasMap[$entityAlias], $rsm->relationMap))); 
     } 

     return $path; 
    } 

    private function attach($object, $field, $place) { 
     if (count($place) > 1) { 
      $prop = $place[0]; 
      array_splice($place, 0, 1); 
      if (!isset($object->{$prop})) $object->{$prop} = new \stdClass(); 
      $this->attach($object->{$prop}, $field, $place); 
     } else { 
      $prop = $place[0]; 
      $object->{$prop} = $field; 
     } 
    } 
} 

$sql = ' 
    SELECT p.*, COUNT(r.id) 
    FROM products p 
    LEFT JOIN reviews r ON p.id = r.product_id 
'; 

$em = $this->getDoctrine()->getManager(); 

$rsm = new ResultSetMapping(); 
$rsm->addEntityResult('SomeBundle\Entity\Product', 'p'); 
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount'); 

$query = $em->createNativeQuery($sql, $rsm); 

$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'SomeBundle\CustomHydrator'); 
$results = $query->getResult('CustomHydrator'); 

誰かを助けることができる希望:

関連する問題