Laravelのpaginateにて、Limitをかける方法

パターン1(処理速度が遅め)

LaravelではPaginateメソッドを使用した場合、limitの制限がPaginateメソッドでのlimitが優先され、上書きされてしまう。
そのため、最大100件としたい場合でも、検索結果の全てが表示されてします。

そこで、ここではlimit制限をかけるために行ったカスタマイズ方法をお伝えする。

まずはよくある失敗事例として、次のコードを記載する。

$lists = Users::->where('id', '>', 10)->limit(100)->paginate(10);

こちらのソースでは、Usersテーブルからidカラム10以上のデータを100件取得し、かつページネーションをつける、という意味合いとなる。

この記述方法にて意図通りの動作が行われればいいものの、「limit(100)」が無視され、paginateメソッド内でのlimitに上書きされてしまい、結果としてwhere句で絞り込んだ全件を結果を返してしまう。

次に、カスタマイズ方法を記載する。カスタマイズを行う場合、Paginateメソッドの元ソースを変更するため、アップグレード等には再度調整を行う等、注意が必要となる。

変更を行うファイルは、
「vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php」
となる。

「Builder.php」ファイルは次の通り。

public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
    $page = $page ?: Paginator::resolveCurrentPage($pageName);

    $total = $this->toBase()->getCountForPagination();

    $perPage = ($perPage instanceof Closure
        ? $perPage($total)
        : $perPage
    ) ?: $this->model->getPerPage();

    $results = $total
        ? $this->forPage($page, $perPage)->get($columns)
        : $this->model->newCollection();

    return $this->paginator($results, $total, $perPage, $page, [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => $pageName,
    ]);
}

これを、次の通りに変更する。

public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $limit = 10000000)
{
    $page = $page ?: Paginator::resolveCurrentPage($pageName);

    $total = $this->toBase()->getCountForPagination();
    if($total > $limit) $total = $limit;
    $perPage = ($perPage instanceof Closure
        ? $perPage($total)
        : $perPage
    ) ?: $this->model->getPerPage();

    $results = $limit
        ? $this->forPage($page, $perPage)->limit($perPage)->get($columns)
        : $this->model->newCollection();

    return $this->paginator($results, $total, $perPage, $page, [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => $pageName,
    ]);
}

変更箇所としては、パラメータに「$limit = 10000000」を追加。

「$total = $this->toBase()->getCountForPagination();」の後に、
「if($total > $limit) $total = $limit;」を追加する。
理由として、設定したlimit以上の場合、limitで上書きを行うためのもの。

$results = $total」を「$results = $limit」に変更する。

? $this->forPage($page, $perPage)->get($columns)」を、
? $this->forPage($page, $perPage)->limit($perPage)->get($columns)」に変更する。

この設定により、limitをかけることが可能となる。

次に使用方法を記載する。
例として、検索パラメータがある場合を想定したものとする。

コントローラは次のとおり。

// パラメータ
$keyword = $request->query('k');
$params = [
    'k' => $keyword
];

// 検索条件
$where[] = ['name', 'like', sprintf('%%s%', $keyword];

// 検索結果取得
// パラメータはpaginateメソッドの通常の設定値と、最後にlimitのための100を追加
$lists = Users::->where($where)->paginate(10, ['*'], 'page', null, 100);

// viewに渡す
return view('search', compact('lists', 'params'));

viewは次のとおり。

<!--
    検索結果を出力
-->
@if(!empty($lists) && $lists->count())
    @foreach($lists as $key => $col)
        <div>{{ $col->name }}</div>
    @endforeach
@endif

<!--
    ページネーションを出力
    appendsメソッドを使用し、検索パラメータ「$params」を設定する。
-->
@if(!empty($lists) && $lists->count())
    {{ $lists->appends($params)->links() }}
@endif

これにより、paginateにてlimitをかけることが可能となった。

パターン2(処理速度が早い)

パターン1の方法では、「Builder.php」にて取得件数を「$this->toBase()->getCountForPagination();」にて取得を行っている。
これは、全件データを取得するためデータ量が多い場合、速度の低下につながる。

そこで、countを使わず取得する流れとする。

public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $limit = 10000000, $limitNew = false)
{
    $page = $page ?: Paginator::resolveCurrentPage($pageName);
    $this->setQuery($this->getQuery()->limit($limit));
    if($limitNew) $total = count($this->getQuery()->get());
    else $total = $this->toBase()->getCountForPagination();
    
    if($total > $limit) $total = $limit;
    $perPage = ($perPage instanceof Closure
        ? $perPage($total)
        : $perPage
    ) ?: $this->model->getPerPage();

    $results = $limit
        ? $this->forPage($page, $perPage)->limit($perPage)->get($columns)
        : $this->model->newCollection();

    return $this->paginator($results, $total, $perPage, $page, [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => $pageName,
    ]);
}

変更箇所としては、パラメータに「$limit = 10000000」の後、「$limitNew = false」を追加。
$this->toBase()->getCountForPagination();」を

 if($limitNew) $total = count($this->getQuery()->get());
 else $total = $this->toBase()->getCountForPagination();

に変更する。

取得方法は「$lists = Users::->where($where)->paginate(10, [‘*’], ‘page’, null, 100, true);」となる。
「true」の場合、countにてlimit後のデータを取得し、「false」の場合、これまで通りとなる。

これにより、高速にデータを取得することが可能となった。

問題点として、limitのデータ量が多くなる場合、メモリーオーバーが発生し、500エラーを発生してしまう。
そのため、limit数が1万未満程度であれば高速化に有効な手段である、とする。