Функция Rand() в Doctrine. Как получить случайные записи в Доктрине.

Функция Rand() в Doctrine. Как получить случайные записи в Доктрине.

В этой статье я расскажу об одной, очень не популярной, однако, неожиданно возникающей проблеме. Сегодня я расскажу, как в Доктрине выбирать случайные записи (на диалекте SQL - записи ORDER BY Rand()).

Любой PHP Symfony разработчик знает о проблеме получения случайных строк/записей с помощью Doctrine. Это связано с тем, что нет встроенного решения, и, к сожалению, команда Doctrine даже не хочет реализовывать эту возможность!

Так как же мы можем получить случайные строки/объекты? Давайте рассмотрим возможные варианты, каждый из которых имеет свои преимущества и недостатки.

1. Добавить собственную Numeric Function:

Для добавления кастомной Numeric функции в Доктрине, определите новый класс в своём проекте следующим образом:

namespace App\Infrastructure\Doctrine\Functions;
 
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\SqlWalker;

class Rand extends FunctionNode
{
    public function parse(Parser $parser)
    {
        $parser->match(Lexer::T_IDENTIFIER);
        $parser->match(Lexer::T_OPEN_PARENTHESIS);
        $parser->match(Lexer::T_CLOSE_PARENTHESIS);
    }

    public function getSql(SqlWalker $sqlWalker)
    {
        return 'RAND()';
    }
}

После чего, вам нужно зарегистрировать эту функцию, чтобы появилась возможность использования её при построении запросов. В результате чего вы можете использовать функцию RAND() непосредственно в любом вашем запросе:

$query->addSelect('RAND() as HIDDEN rand')->orderBy('RAND()');

Пример регистрации подобной функции в моём проекте на Slim, выглядит так:

//...
$config->addCustomNumericFunction('Rand', \App\Infrastructure\Doctrine\Functions\Rand::class)

Или для Symfony.

2. Используйте Native SQL в Doctrine

Во-первых, вы можете передать "сырой SQL-запрос" для получения случайных записей с помощью MySQL'евского ORDER BY RAND(). Затем можете загрузить связанные объекты, используя нативный SQL в доктрине, как это проиллюстрировано ниже:

// получаем случайные идентификаторы выполняя сырой MYSQL запрос
$stmt = $conn->prepare('SELECT id from `table` ORDER BY RAND() LIMIT 100');
$stmt->execute();

$random_ids = [];
while ($val = $stmt->fetch()) {
    $random_ids[] = $val['id'];
}

// нативный SQL в доктрине для загрузки связанных объектов
$query = $this->em->createQuery("SELECT tt
     FROM AppBundle:table tt
     WHERE tt.id in (:ids)"
)->setParameter('ids', $random_ids);

$randomEntities = $query->getResult();

Таким образом, мы сможем получить случайные записи даже при сложных join-ах. Для более быстрой работы с большими наборами данных, обратите внимание на этот пост StackOverflowоригинальную ссылку), где показано, как можно использовать нативный SQL, для получения случайных записей.

3. Использовать библиотеку DoctrineExtensions

Установите библиотеку в соответствии с инструкцией и обновите ваш конфигурационный файл config.yml.

// config.yml
doctrine:
     orm:
         dql:
             numeric_functions:
                 rand: DoctrineExtensions\Query\Mysql\Rand

После чего вы можете просто использовать функцию RAND(), определённую в этой библиотеке с помощью Конструктора Запросов (Query Builder-а):

$query->addSelect('column')->orderBy('RAND()'); 

4. Используйте PHP для получения случайных строк/записей.

Когда у вас есть лишь небольшой набор данных (менее 10k записей), то вы можете перетасовать их с помощью PHP.

// получаем все записи
$tasks = $em->getRepository('Entity\Task')->findAll();
// перемешаваем их
shuffle($tasks);
// достаём случайную запись и работаем с ней

Недостатком этого метода является то, что он будет довольно неэффективным, если у вас будет огромный набор данных (100k+ записей).

Бонус

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

// получаем общее количество всех записей в таблицу 
$rows = $em->createQuery('
        SELECT COUNT(u.id) 
        FROM AcmeUserBundle:User u'
    )
    ->getSingleScalarResult();

// считаем случайный отступ
$offset = max(0, rand(0, $rows - $n - 1));

//получаем первые $n записей (пользователей), начиная с вычесленного нами случайного значения
$query = $em->createQuery('
        SELECT DISTINCT u
        FROM AcmeUserBundle:User u'
    )
    ->setMaxResults($n)
    ->setFirstResult($offset);

$result = $query->getResult(); 

$n объектов пользователей, которые вы получаете последовательно (т.е. i-й, (i+1)-й,...,(i+$n)-й). Это эффективная альтернатива, которая отлично подходит для случаев, когда необходимость взять одну или две случайные сущности, а не весь список.

Надеюсь, эта статья помогла вам узнать, как получить случайную запись в Doctrine, и добавить эту функциональность в Slim, Symfony, или же собственный фреймворк, использующий доктрину. Теперь вы без проблем можете отсортировать данные в проект случайным образом (используя Rand()), расширив стандартные функциональные возможности Doctrine.