Доктрина: разбиение на страницы с левым соединением

Я хотел бы разбивать сложный запрос, по крайней мере, на два левых соединения, но пакет разбиения на страницы, который я использую (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 */
    );

Любая идея о том, как разбивать страницы на сложный запрос, я вполне уверен, что этот прецедент распространен, не хочу гидратировать мой результат после разбивки на страницы.

Ответы

Ответ 1

Для тех, кто ищет ответ об этом, есть хорошее решение: https://github.com/KnpLabs/KnpPaginatorBundle/blob/master/Resources/doc/manual_counting.md

$paginator = new Paginator;

$count = $entityManager
->createQuery('SELECT COUNT(c) FROM Entity\CompositeKey c')
->getSingleScalarResult();

$query = $entityManager
->createQuery('SELECT c FROM Entity\CompositeKey c')
->setHint('knp_paginator.count', $count);

$pagination = $paginator->paginate($query, 1, 10, array('distinct' => false));

В основном, что вы делаете, вы создаете свой собственный запрос "count" и инструктируете papinator knp использовать это. Не забудьте добавить

Ответ 2

Трудно понять сущности в исходном вопросе, но я столкнулся с этой проблемой и действительно разрешился.

Предположим, что у вас есть такая сущность:

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

Важно то, что он имеет отношения "один ко многим" с другим объектом, и вы хотите присоединиться к нему с помощью QueryBuilder по любой причине (например, вы можете добавить HAVING в ваш запрос, чтобы выбрать только те объекты с одним или несколькими из этих других объектов).

Оригинальный код может выглядеть так:

  $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 имел только один компонент "от" - точно так же, как указывает исключение, - перемещая соединение inline. Например:

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

Должно отлично работать с классом класса "Учение", без каких-либо других пакетов. Также обратите внимание на сокращенный синтаксис соединения; с Доктриной, которую вы не видели, чтобы указать объединенную сущность явно (хотя вы можете), поскольку отображение уже знает, что это такое.

Надеюсь, это поможет кому-то, там не так много по этому вопросу. Вы можете узнать больше о том, как внутренне справляются с этим, просмотрев комментарий @halfer здесь.

Ответ 3

Обновление доктрины /dbal до 2.5 исправлено для меня.

Ответ 4

Если вы не работаете с обычными сопоставленными объектами способом, указанным в ответах выше, объединения будут создавать дополнительные компоненты в результирующем наборе. Это означает, что набор не может считаться, поскольку он не является скалярным результатом.

Таким образом, ключ сохраняет ваши результирующие наборы в один компонент, который позволит производить и подсчитывать скалярный результат.

Вы можете преобразовать регулярное поле в сопоставление сущности во время выполнения. Это позволяет вам писать запросы с возвратом только одного компонента, даже если вы произвольно присоединяетесь к другим объектам. "Другие сущности" становятся дочерними элементами основной сущности, а не дополнительной корневой сущностью или дополнительным компонентом результата.

В приведенном ниже примере показано взаимно однозначное отображение. Я не пробовал более сложные сопоставления, но мой успех с этим предполагает, что это должно быть возможно. У объекта регистрации есть поле, называемое пользователем, которое содержит идентификатор пользователя, но его не отображенный объект просто простое поле.

(Это было изменено из рабочего примера, но не проверено как есть - поэтому рассматривайте как псевдокод)

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

Примечание. Я использовал PagerFanta, а не KNP, но проблема заключается в Doctrine, поэтому я ожидаю, что это сработает где угодно.