0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravelのpaginatorで件数カウントとデータの取得が同時に行われるのを防ぐ

Last updated at Posted at 2024-08-14

何がしたいか

ページが重いので軽くしたい。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アプリケーションフレームワーク使っていると難しい気もする。

リプレースできるならとっとと変えちゃうと良いような気もします。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?