🎻

[Symfony][Doctrine] JOINされたクエリのルートエンティティのみにcount/offset/limitを適用する方法

2020/07/29に公開

やりたいこと

以下のような、子エンティティや孫エンティティの内容を検索条件に使うクエリを考えてみましょう。

$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()
;

実はこれだと意図した結果が得られません🤔

JOINされているクエリではルートエンティティのみを count offset limit できない

子エンティティや孫エンティティが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
  1. QueryBuilderをもとにPaginatorを作る
  2. 件数のカウントはPaginator自体を count() すればOK
  3. offset/limitは、Paginatorから getQuery() したクエリに対して setFirstResult() setMaxResults() を適用しておいた上で、Paginatorの getIterator() で取得

という感じです。

ちなみに pagerfanta実装でもPaginatorが使われています

まとめ

  • Doctrineで、JOINされているクエリに対してルートエンティティのみにcount/offset/limitを適用するには、Doctrine ORM ToolsのPaginator
GitHubで編集を提案

Discussion