こんにちは、かつコーチです。
前回はN+1問題とeager loadingについて解説しました。
今回は、一覧画面を作るときに必ずと言っていいほど必要になる「ページネーション」を扱います。
Laravelにはpaginate()という便利なメソッドがあり、これを使うだけでLengthAwarePaginatorという仕組みが自動的に動いてくれます。
一見ブラックボックスに見えるこの仕組みを、中身から理解していきましょう。
ページネーションとLengthAwarePaginatorの基本
LengthAwarePaginatorとは
LengthAwarePaginatorとは、データの総件数を把握したうえでページ番号付きのリンクを生成できる、Laravelのページネーション機能の中核クラスです。
「Length(長さ)をAware(把握)している」という名前の通り、全体の件数がわかっているのが特徴です。
そのため「全◯件中◯〜◯件を表示」「最後のページ番号」といった、総件数ベースの表示ができます。
Eloquentモデルに対してpaginate()メソッドを呼ぶだけで、内部的にこのLengthAwarePaginatorのインスタンスが生成されます。
<?php
$posts = Post::orderBy('created_at', 'desc')->paginate(10);
これだけで、10件ごとに区切られたページネーション済みのデータが手に入ります。
なぜページネーションが必要なのか
一覧画面で全件を一度に表示してしまうと、データが増えるほどページの表示が遅くなります。
ブログ記事が1,000件あるのに、それを1ページに全部表示していたら、読み込みだけで一苦労です。
さらに、SQLの発行件数やメモリ使用量も増えるため、サーバー側の負荷も無視できません。
ページネーションを使えば、「今表示している10件だけ」をデータベースから取得できるので、件数が増えても表示速度がほぼ変わらない設計にできます。
paginate()の基本的な使い方
コントローラでの実装
まずは一覧画面でよくある実装を見てみましょう。
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$posts = Post::orderBy('created_at', 'desc')->paginate(10);
return view('posts.index', compact('posts'));
}
}
paginate(10)とすることで、「1ページあたり10件」のLengthAwarePaginatorインスタンスが$postsに入ります。
このとき、Laravelは内部で2種類のクエリを発行しています。
1つは実際に表示する10件を取得するSELECT文、もう1つは全体の件数を数えるCOUNT文です。
この2つのクエリのおかげで、「全◯件中」という表示や「最後のページ番号」の計算が可能になっています。
ビューでのリンク表示
ビュー側では、links()メソッドを呼ぶだけでページネーションリンクが自動生成されます。
{{-- resources/views/posts/index.blade.php --}}
<div>
@foreach ($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
<p>{{ $post->created_at->format('Y年m月d日') }}</p>
</article>
@endforeach
</div>
<div class="pagination">
{{ $posts->links() }}
</div>
$posts自体は通常のコレクションと同じように@foreachでループできます。
そのうえで{{ $posts->links() }}と書くだけで、「1 2 3 … 次へ」といったページ番号のリンクが自動で描画されます。
デフォルトではBootstrap用のCSSクラスが付いたHTMLが出力されますが、Tailwind CSS環境でも標準対応済みなので、そのまま使ってもある程度崩れずに表示されます。
LengthAwarePaginatorの内部を理解する
simplePaginate()との違い
Laravelにはpaginate()のほかに、simplePaginate()というメソッドもあります。
この2つの違いは、総件数を数えるCOUNTクエリを発行するかどうかです。
| メソッド | 総件数のCOUNT | 「最後のページ」表示 | 向いているケース |
|---|---|---|---|
paginate() | 発行する | 可能 | 通常の一覧画面(ページ番号を全部表示したい) |
simplePaginate() | 発行しない | 不可(前へ/次へのみ) | 件数が膨大で、COUNTクエリ自体が重いテーブル |
simplePaginate()はCOUNTクエリを省略する分、パフォーマンスは有利ですが、「全部で何ページあるか」がわからないため「前へ」「次へ」のリンクしか出せません。
管理画面の一覧のように「最後のページまで何ページか知りたい」場面ではpaginate()、SNSのタイムラインのように「とにかく次を読み込めればいい」場面ではsimplePaginate()、という使い分けが基本です。
withQueryString()でクエリパラメータを維持する
一覧画面に検索・絞り込み機能がある場合、ページ送りをすると検索条件が消えてしまうことがあります。
私が実際にこれでつまずいたのは、キーワード検索付きの一覧画面を作っていたときでした。
❌ Before:検索条件がページ送りで消えてしまう
<?php
public function index(Request $request)
{
$keyword = $request->input('keyword');
$posts = Post::when($keyword, function ($query) use ($keyword) {
$query->where('title', 'like', "%{$keyword}%");
})->paginate(10);
return view('posts.index', compact('posts', 'keyword'));
}
このままだと、links()が生成するURLには?page=2しか付かず、?keyword=Laravelのような検索条件が消えてしまいます。
その結果、2ページ目に飛んだ瞬間に検索結果ではなく全件一覧に切り替わってしまい、「あれ、さっきの検索結果はどこへ?」と何度も首をかしげました。
✅ After:withQueryString()でクエリパラメータを維持する
<?php
public function index(Request $request)
{
$keyword = $request->input('keyword');
$posts = Post::when($keyword, function ($query) use ($keyword) {
$query->where('title', 'like', "%{$keyword}%");
})->paginate(10)->withQueryString();
return view('posts.index', compact('posts', 'keyword'));
}
withQueryString()を1行足すだけで、現在のクエリパラメータ(keyword=Laravelなど)を保持したままlinks()のURLが生成されるようになります。
検索・絞り込み機能付きの一覧画面を作るときは、paginate()とセットで覚えておくと事故を防げます。
ページネーションの応用
appends()で特定のパラメータだけ維持する
withQueryString()は現在のリクエストに含まれる全パラメータを維持しますが、特定のパラメータだけを維持したい場合はappends()を使います。
<?php
$posts = Post::paginate(10)->appends(['sort' => 'newest']);
withQueryString()はLaravel 8以降で使える書き方で、基本的にはこちらの方が記述量が少なく済みます。
「特定の1つのパラメータだけ意図的に付けたい」という限定的なケースでのみ、appends()を選ぶとよいでしょう。
APIレスポンスとしてページネーションを返す
Bladeビューだけでなく、API開発でもpaginate()はそのまま活用できます。
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
class PostApiController extends Controller
{
public function index()
{
$posts = Post::orderBy('created_at', 'desc')->paginate(15);
return response()->json($posts);
}
}
LengthAwarePaginatorはIlluminate\Contracts\Support\Jsonableを実装しているため、response()->json()に渡すだけで、データ本体に加えてcurrent_pageやlast_page、totalなどのメタ情報を含んだJSONが自動生成されます。
フロントエンド側でページネーションUIを自作する際も、このメタ情報をそのまま使えるので、APIとBladeビューの両方で同じpaginate()が使い回せるのは大きなメリットです。
まとめ
この記事のポイント
- LengthAwarePaginatorは総件数を把握したうえでページネーションを実現する、
paginate()の内部で動く仕組み paginate()はCOUNTクエリを発行して総ページ数を計算し、simplePaginate()は発行せず軽量に「前へ/次へ」だけ提供する- 検索・絞り込み条件付きの一覧画面では
withQueryString()を忘れるとページ送りで条件が消える - APIレスポンスとして返す場合も
paginate()の結果をそのままresponse()->json()に渡せる
次に読むべき記事
一覧画面の実装ができたら、次はデータの更新処理を安全に行うための「トランザクション」を学んでいきましょう。
→ 次の記事:トランザクション(DB::transaction)でデータ整合性を守る