В Laravel 7 появилась любопытная возможность — использование «ленивых коллекций» — на основе генераторов.
Возможность очень полезная, если не сказать революционная.
Задача — сформировать большой отчёт и выгрузить его в xls файл. В отчёте могут быть десятки тысяч записей.
Итак — решение в лоб — выбрать из базы данные из нужной таблицы, далее построчно в цикле формировать отчёт.
Грубо говоря:
$users = DB::table('users')->get();
//открываем файл отчёта
foreach($users as $user){
//построчно заполняем
}
//выводим
Тут плохо всё — сразу получаются все записи, проход по которым будет идти очень долго и съест всю доступную память.
Решение по старому. Мы формируем задачу на создание отчёта и кладём её в очередь, чтобы не зависеть от времени, за которое он сформируется. Для этого у нас есть инструмент horizon, но в принципе можно использовать любую другую очередь. Переодически спрашиваем — готов ли отчёт.
Для выборки из базы используем возможность выбирать записи по частям, чтобы не переполнять память.
//Открываем файл $report
DB::table('users')->chunk(10000, function ($users, $report) {
foreach ($users as $user) {
//Заполняем файл
}
});
//Завершаем обработку
Минусы этого решения — нельзя работать со всей коллекцией сразу, что бывает очень удобно. Приходится использовать некую третью переменную, чтобы доставить результаты обработки, поскольку они находятся в замыкании. И вопрос памяти также стоит, хотя и менее остро, поскольку мы можем задать случайно слишком большое число записей, которое будет работать, но при данной нагрузке окажется вдруг избыточным. Проверить это довольно проблематично.
Решением этих проблем стали коллекции на основе генераторов .
//Получаем данные в виде обычной коллекции
$data = $this->getData($request);
//Оборачиваем в генератор
$users = LazyCollection::make(function ()
use ($data) {
foreach ($data as $item) {
yield $item;
}
});
// Либо с использованием базы, учётом лимита и страниц
{
$lazyItems = LazyCollection::make(function ()
use ($queryBuilder, $page, $limitPage, $recentItem, $countItems) {
while ($recentItem < $countItems) {
//находим данные 1 запросом, как и было раньше и оборачиваем элементы в LazyCollection
$items = $queryBuilder->forPage($page, $limitPage)->get();
foreach ($items as $item) {
yield $item;
}
$recentItem = $recentItem + $limitPage;
$page++;
}
});
return $lazyItems;
}
Здесь по сути обычный генератор, который будет выбрасывать по 1 элементу, вместо всего набора из базы. Таким образом у нас колоссально экономится память. Нужно только передать в этот метод сам запрос к базе данных, страницу, лимит количества элементов на 1 страницу.
Получив данные в виде LazyCollection мы можем работать с ними так, как будто у нас обычная небольшая коллекция, то есть идти по ним перебором, не опасаясь падения.
Поскольку мы возвращаем всегда только 1 элемент, это наиболее оптимизированная к нагрузкам версия получения чего-либо из базы.
Конечно при этом наш скрипт по — прежнему может упасть по времени, но тут ничего не сделаешь без многопоточного программирования либо без отправки задачи в очередь. Причём отправка в очередь является по моему мнению лучшим решением, поскольку не так сильно съедает ресурсы.