何がしたいか
ページが重いので軽くしたい。paginate使ってるのでデータ取得と件数カウントで同時に2発クエリが走っていて、データ取得が遅いクエリであればある程鬱陶しい。というかそもそもなんでpaginate使ってるの・・・?これページングはしないよ・・・?というのはあるんですけど、まぁそこ愚痴っても仕方ない。
前提
Laravel 6 古いです。変えたいです。
DBはmysql 8.0(昔なのでパッチバージョンは忘れました)
どうするか
まぁ大体これが全てなんですが
Eloquentの方のpaginateがこう
/**
* Paginate the given query.
*
* @param int|null $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*
* @throws \InvalidArgumentException
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$perPage = $perPage ?: $this->model->getPerPage();
$results = ($total = $this->toBase()->getCountForPagination())
? $this->forPage($page, $perPage)->get($columns)
: $this->model->newCollection();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
そしてQuery Builderの方のpaginateがこう
/**
* Paginate the given query into a simple paginator.
*
* @param int $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$total = $this->getCountForPagination();
$results = $total ? $this->forPage($page, $perPage)->get($columns) : collect();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
要は内部でforPageで部分的にgetしてその結果をセットしたLengthAwarePaginatorを返せばええんやろ、ってことで、こんなのを書きました。$builder参照渡しにしてるのは「重いんじゃね?」というイメージ先行です。激重クエリ書いてなければ別段必要じゃない気がするのでその辺はお好みで。
(2024/08/16追記:PHPはそもそもclassだとコピーしないよってコメント貰ったんで直しました)
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
class CustomPaginator
{
public static function withoutCountQueryPaginate(
// &は要らない
// QueryBuilder|EloquentBuilder &$builder,
QueryBuilder|EloquentBuilder $builder,
$perPage = 15,
$columns = ['*'],
$pageName = 'page',
$page = null
){
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$results = $builder->forPage($page, $perPage)->get($columns);
$total = 0;
if($results->isNotEmpty()){
$total = $results->first()->aggregate;
}
return self::paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
/**
* Create a new length-aware paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param int $total
* @param int $perPage
* @param int $currentPage
* @param array $options
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected static function paginator($items, $total, $perPage, $currentPage, $options)
{
return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
'items', 'total', 'perPage', 'currentPage', 'options'
));
}
}
注意
元記事にもあったけど、distinctやunion使ってると全体をサブクエリ化してカウント、みたいな周りくどい方法にならざるを得ず、場合によっては激遅になるので注意。
そもそもpaginateが遅いのもmysql8.0だったので
どうやら、 MySQL 8.0 ではテーブル全件に対するSELECT COUNT(*)の処理で、バッファプールがいっぱいになり古いデータページを追い出す必要がある状況下の処理に問題を抱えている様子 です。
という問題だった可能性大。
以下の記事の様な対処も可能かもですが、indexを動的に指定しなければいけないとDB::rawとか使わざるを得ずLaravel、というかWebアプリケーションフレームワーク使っていると難しい気もする。
リプレースできるならとっとと変えちゃうと良いような気もします。