[Symfony][Doctrine] JOINされたクエリのルートエンティティのみにcount/offset/limitを適用する方法
やりたいこと
以下のような、子エンティティや孫エンティティの内容を検索条件に使うクエリを考えてみましょう。
$parentRepository->createQueryBuilder('p')
->leftJoin('p.children', 'c')
->leftJoin('c.children', 'gc')
->orWhere('c.name like :query')
->orWhere('gc.name like :query')
->setParameter('query', '%'.str_replace('%', '\%', $criteria->query).'%')
;
ここで、
- 検索した結果に対して
- 親のみの総件数を知りたい
- 先頭の20件を飛ばして、それ以降の10件を取得したい
という要件があるとします。
何も考えずに普通に実装しようとすると以下のようなコードを書いてしまうのではないでしょうか。
$qb = $parentRepository->createQueryBuilder('p')
->leftJoin('p.children', 'c')
->leftJoin('c.children', 'gc')
->orWhere('c.name like :query')
->orWhere('gc.name like :query')
->setParameter('query', '%'.str_replace('%', '\%', $criteria->query).'%')
;
$parentCount = (int) $qb
->select('count(p)')
->getQuery
->getSingleScalarResult()
;
$parentSlice = $qb
->select('p')
->setFirstResult(20)
->setMaxResults(10)
->getQuery()
->getResult()
;
実はこれだと意図した結果が得られません🤔
count
offset
limit
できない
JOINされているクエリではルートエンティティのみを 子エンティティや孫エンティティがJOINされているため、
-
count
も -
setFirstResult()
(offset
)も -
setMaxResults()
(limit
)も
JOINされた結果に対して適用されてしまうためです。
Doctrine ORMの公式ドキュメントでも以下のとおり言及されています。
If your query contains a fetch-joined collection specifying the result limit methods are not working as you would expect. Set Max Results restricts the number of database result rows, however in the case of fetch-joined collections one root entity might appear in many rows, effectively hydrating less than the specified number of results.
Doctrine Query Language - Doctrine Object Relational Mapper (ORM)
普通にQueryBuilderを使う方法だと、クエリのルートエンティティである親エンティティだけを count
offset
limit
することはできないというわけですね。
参考:symfony - doctrine querybuilder limit and offset - Stack Overflow
解決方法:Doctrine ORM ToolsのPaginatorを使う
このような場合は、Doctrine ORM ToolsのPaginator を使うことでやりたいことが実現できます。
use Doctrine\ORM\Tools\Pagination\Paginator;
// ...
$qb = $parentRepository->createQueryBuilder('p')
->leftJoin('p.children', 'c')
->leftJoin('c.children', 'gc')
->orWhere('c.name like :query')
->orWhere('gc.name like :query')
->setParameter('query', '%'.str_replace('%', '\%', $criteria->query).'%')
;
$paginator = new Paginator($qb);
$parentCount = count($paginator); // int
$paginator->getQuery()
->setFirstResult(20)
->setMaxResults(10)
;
$parentSlice = $paginator->getIterator(); // \ArrayIterator
- QueryBuilderをもとにPaginatorを作る
- 件数のカウントはPaginator自体を
count()
すればOK - offset/limitは、Paginatorから
getQuery()
したクエリに対してsetFirstResult()
setMaxResults()
を適用しておいた上で、PaginatorのgetIterator()
で取得
という感じです。
ちなみに pagerfanta の 実装でもPaginatorが使われています。
まとめ
- Doctrineで、JOINされているクエリに対してルートエンティティのみにcount/offset/limitを適用するには、Doctrine ORM ToolsのPaginator
Discussion