Laravel - Сбор с отношениями занимает много времени
Мы разрабатываем API с помощью LUMEN. Сегодня у нас была запутанная проблема с получением коллекции нашей модели "TimeLog". Мы просто хотели получить все время журналы с дополнительной информацией от модели платы и модели задачи. В одном ряду журнала времени у нас были board_id и task_id. Это соотношение 1:1 на обоих.
Это был наш первый код для получения всей информации. Это заняло много времени, а иногда мы получали тайм-аут:BillingController.php
public function byYear() {
$timeLog = TimeLog::get();
$resp = array();
foreach($timeLog->toArray() as $key => $value) {
if(($timeLog[$key]->board_id && $timeLog[$key]->task_id) > 0 ) {
array_push($resp, array(
'board_title' => isset($timeLog[$key]->board->title) ? $timeLog[$key]->board->title : null,
'task_title' => isset($timeLog[$key]->task->title) ? $timeLog[$key]->task->title : null,
'id' => $timeLog[$key]->id
));
}
}
return response()->json($resp);
}
TimeLog.php, где была установлена связь.
public function board()
{
return $this->belongsTo('App\Board', 'board_id', 'id');
}
public function task()
{
return $this->belongsTo('App\Task', 'task_id', 'id');
}
Наш новый способ выглядит так:BillingController.php
public function byYear() {
$timeLog = TimeLog::
join('oc_boards', 'oc_boards.id', '=', 'oc_time_logs.board_id')
->join('oc_tasks', 'oc_tasks.id', '=', 'oc_time_logs.task_id')
->join('oc_users', 'oc_users.id', '=', 'oc_time_logs.user_id')
->select('oc_boards.title AS board_title', 'oc_tasks.title AS task_title','oc_time_logs.id','oc_time_logs.time_used_sec','oc_users.id AS user_id')
->getQuery()
->get();
return response()->json($timeLog);
}
Мы удалили отношение в TimeLog.php, потому что оно нам больше не нужно. Теперь у нас есть время загрузки около 1 секунды, что хорошо! В таблице журнала времени есть около 20 тысяч записей.
Мои вопросы:
- Почему первый метод выходит за пределы допустимого диапазона (что вызывает тайм-аут?)
- Что делает getQuery(); точно делать?
Если вам нужна дополнительная информация, просто спросите меня.
2 ответа
--Первый вопрос--
Одна из проблем, с которой вы можете столкнуться, - это хранение всех этих огромных объемов данных в памяти, а именно:
$timeLog = TimeLog::get();
Это уже огромно. Затем, когда вы пытаетесь преобразовать коллекцию в массив:
- Существует цикл по коллекции.
- С использованием
$timeLog->toArray()
хотя инициализация цикла, основанная на моем понимании, неэффективна (хотя, возможно, я не совсем прав в этом) - Тысячи запросов сделаны для получения связанных моделей
Итак, я бы предложил пять методов (один, который спасет вас от сотен запросов), а последний - эффективный при возвращении результата в соответствии с индивидуальными настройками:
Так как у вас есть много данных, то
chunk
результат ref: Laravel chunk, так что вместо этого у вас есть:$timeLog = TimeLog::chunk(1000, function($logs){ foreach ($logs as $log) { // Do the stuff here } });
Другой способ - использовать курсор (запускает только один запрос, если условия совпадают). Внутренняя операция курсора, как это понимается, - использование Генераторов.
foreach (TimeLog::where([['board_id','>',0],['task_id', '>', 0]])->cursor() as $timelog) { //do the other stuffs here }
Это выглядит как первый, но вместо этого вы уже сузили свой запрос до того, что вам нужно:
TimeLog::where([['board_id','>',0],['task_id', '>', 0]])->get()
Eager Loading уже предоставит вам необходимые отношения на лету, но может также привести к увеличению объема данных в памяти. Поэтому, возможно, метод chunk облегчит управление (даже если вы загружаете связанные модели)
TimeLog::with(['board','task'], function ($query) { $query->where([['board_id','>',0],['task_id', '>', 0]]); }])->get();
Вы можете просто использовать Transformer
- С помощью преобразователя вы можете загружать связанную модель элегантными, чистыми и более контролируемыми методами, даже если размер огромен, и еще одно преимущество заключается в том, что вы можете преобразовать результат, не беспокоясь о том, как его обвить. Вы можете просто обратиться к этому ответ, чтобы выполнить простое использование этого. Однако, если вам не нужно преобразовывать свой ответ, вы можете выбрать другие варианты.
Хотя это не может полностью решить проблему, но поскольку основные проблемы, с которыми вы сталкиваетесь, основаны на управлении памятью, то приведенные выше методы должны быть полезны.
Второй вопрос
Основываясь на Laravel API здесь вы можете увидеть, что:
Он просто возвращает базовый экземпляр построителя запросов. На мой взгляд, это не нужно на основе вашего примера.
ОБНОВИТЬ
На вопрос 1, поскольку кажется, что вы хотите просто вернуть результат как ответ, честно говоря, более эффективно разбивать этот результат на страницы. Laravel предлагает нумерацию страниц. Самым простым из них является SimplePaginate, который хорош. Единственное, что он делает еще несколько запросов к базе данных, но проверяет последний индекс; Я думаю, что он использует cursor
как хорошо, но не уверен. Я думаю, наконец, это может быть более идеальным, имея:
return TimeLog::paginate(1000);
Я столкнулся с подобной проблемой. Основная проблема здесь заключается в том, что Elloquent действительно медленно выполняет огромные задачи, потому что он извлекает все результаты одновременно, поэтому краткий ответ заключается в том, чтобы извлекать его строка за строкой, используя выборку PDO.
Краткий пример:
$db = DB::connection()->getPdo();
$query_sql = TimeLog::join('oc_boards', 'oc_boards.id', '=', 'oc_time_logs.board_id')
->join('oc_tasks', 'oc_tasks.id', '=', 'oc_time_logs.task_id')
->join('oc_users', 'oc_users.id', '=', 'oc_time_logs.user_id')
->select('oc_boards.title AS board_title', 'oc_tasks.title AS task_title','oc_time_logs.id','oc_time_logs.time_used_sec','oc_users.id AS user_id')
->toSql();
$query = $db->prepare($query->sql);
$query->execute();
$logs = array();
while ($log = $query->fetch()) {
$log_filled = new TimeLog();
//fill your model and push it into an array to parse it to json in future
array_push($logs,$log_filled);
}
return response()->json($logs);