Функция 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.