web-dev-qa-db-ja.com

Doctrine:左結合によるページネーション

少なくとも2つの左結合を持つ複雑なリクエストをページ付けしたいのですが、使用しているページ付けバンドル(KnpPaginationBundle)は、結果のカウント方法をDoctrineに伝えることができません( これはページ付けプロセス )に必要であり、この例外が発生し続けます。

Cannot count query which selects two FROM components, cannot make distinction

これは、Doctrine QueryBuilderで作成されたサンプルリクエストです。

public function findGroupsByUser(User $user, $listFilter, $getQuery = false, $order = 'ASC')
{
    $query = $this->createQueryBuilder('r')
        ->select('r as main,g')
        ->select('r as main,g, count(gg) as members')
        ->leftjoin('r.group', 'g')
        ->innerjoin('MyBundle:GroupMemberRel', 'gg', 'WITH', 'r.group = gg.group')
        ->addGroupBy('g.groupId')
        ->add('orderBy', 'g.name ' . $order);
   if ($getQuery == true) {
        return $query;
    }

    return $query->getQuery()->getResult();
}

次に、このリクエストをknp_paginatorサービスに送信すると、例外が発生します

    $groupQuery = $this->em->getRepository('MyBundle:GroupMemberRel')->findGroupsByUser($user, $listFilter, true);
    $paginator = $this->container->get('knp_paginator');
    /* @var $groups Knp\Component\Pager\Pagination\PaginationInterface */
    $groups = $paginator->paginate(
        $groupQuery, $this->container->get('request')->query->get('page', 1), 10 /* limit per page */
    );

複雑なリクエストをページ分割する方法についてのアイデアは、このユースケースが一般的であると確信しています。ページ付け後に結果をハイドレイトしたくありません。

これについての答えを探している人には、次の場所に良い解決策があります: https://github.com/KnpLabs/KnpPaginatorBundle/blob/master/Resources/doc/manual_counting.md

$paginator = new Paginator;

// determine the count of the entire result set
$count = $entityManager
    ->createQuery('SELECT COUNT(c) FROM Entity\CompositeKey c')
    ->getSingleScalarResult();

// create a query to be used with the paginator, and specify a hint
$query = $entityManager
     ->createQuery('SELECT c FROM Entity\CompositeKey c')
     ->setHint(
        'knp_paginator.count', 
        $count
    );

// paginate, and set "disctinct" option to false
$pagination = $paginator->paginate(
    $query, 
    1, 
    10, 
    array(
        'distinct' => false,
    )
);

基本的に、あなたがしていることは、あなた自身の「カウント」クエリを作成し、これを使用するようにknpページネーションに指示することです。

22
mentalic

元の質問の実体を理解するのは難しいですが、私はこれと同じ問題に遭遇し、それは確かに解決可能でした。

次のようなエンティティがあるとします。

  class User
  {
    // ...
    /**
     * @ORM\OneToMany(targetEntity="Registration", mappedBy="user")
     */
    private $registrations;
    // ...
  }

重要なことは、それが別のエンティティと1対多の関係にあり、何らかの理由でQueryBuilderを介してそれに参加できるようにすることです(たとえば、HAVINGを追加したい場合があります)。これらの他のエンティティの1つ以上を持つエンティティのみを選択するクエリの句)。

元のコードは次のようになります。

  $qb = $this->createQueryBuilder();

  $query = $qb
    ->select('u')
    ->add('from', '\YourBundle\ORM\Model\User u')
    ->leftJoin('\YourBundle\ORM\Model\Registration', 'r', 'WITH', 'u.id = r.user')
    ->groupBy('u.id')
    ->having($qb->expr()->gte($qb->expr()->count('u.registrations'), '1')
    ->getQuery();

これにより、例外がスローされます:Cannot count query which selects two FROM components, cannot make distinction

これを修正するには、結合をインラインに移動して、QueryBuilderに「from」コンポーネントが1つだけになるように(例外が示すとおりに)クエリを書き直します。そのようです:

  $qb = $this->createQueryBuilder();

  $query = $qb
    ->select('u')
    ->add('from', '\YourBundle\ORM\Model\User u LEFT JOIN u.registrations r')
    ->groupBy('u.id')
    ->having($qb->expr()->gte($qb->expr()->count('r'), '1')
    ->getQuery();

DoctrineのPaginatorクラスで問題なく動作し、他のバンドルは必要ありません。結合の省略構文にも注意してください。 with Doctrineマッピングはすでにそれが何であるかを知っているので、結合されたエンティティを明示的に指定することはありません(可能ですが)。

これが誰かを助けてくれることを願っています、この問題についてはそれほど多くはありません。内部がこれをどのように処理するかについて詳しくは、 @ halferのコメントはこちら をご覧ください。

14
futureal

更新中doctrine/dbalバージョン2.5これを修正しました。

3
Jonathan

上記の回答で提供されている方法で通常のマップされたエンティティを操作していない場合、結合によって結果セットに追加のコンポーネントが作成されます。これは、スカラー結果ではないため、セットをカウントできないことを意味します。

したがって、重要なのは、結果セットを1つのコンポーネントに保持することです。これにより、スカラー結果を生成してカウントできるようになります。

実行時に、通常のフィールドをエンティティマッピングに変換できます。これにより、他のエンティティをランダムに結合している場合でも、1つのコンポーネントのみが返されるクエリを作成できます。 「その他のエンティティ」は、結果の追加のルートエンティティまたは追加のコンポーネントではなく、メインエンティティの子になります。

以下の例は、1対1のマッピングを示しています。これ以上複雑なマッピングは試していませんが、これで成功したことは、それが可能であることを示唆しています。登録エンティティには、ユーザーIDを含むuserというフィールドがありますが、マップされたエンティティではなく、単なるフィールドです。

(これは実際の例から変更されていますが、そのままテストされていません-したがって、疑似コードとして扱います)

    $queryBuilder // The query builder already has the other entities added.
        ->innerJoin('r.user', 'user')
    ;


    $mapping['fieldName'] = 'user';
    $mapping['targetEntity'] = '\YourBundle\ORM\Model\User';
    $mapping['sourceEntity'] = '\YourBundle\ORM\Model\Registration';
    $mapping['sourceToTargetKeyColumns'] = array('user' => 'id');
    $mapping['targetToSourceKeyColumns'] = array('id' => 'user');
    $mapping['fetch'] = 2;
    $mapping['joinColumns'] = array(
        array(
            'name' => 'user',
            'unique' => false,
            'nullable' => false,
            'onDelete' => null,
            'columnDefinition' => null,
            'referencedColumnName' => 'id',
        )
    );
    $mapping['mappedBy'] = 'user';
    $mapping['inversedBy'] = null; // User doesn't have to link to registrations
    $mapping['orphanRemoval'] = false;
    $mapping['isOwningSide'] = true;
    $mapping['type'] = ClassMetadataInfo::MANY_TO_ONE;

    $vm = $this->em->getClassMetadata('YourBundle:Registration');

    $vm->associationMappings["user"] = $mapping;

注:私はKNPではなくPagerFantaを使用していましたが、問題はDoctrineにあるため、これはどこでも機能すると思います。

0
Henry