◀ 50.

【Laravel】ページング(ページネーション)をする方法

▶

Laravelのページングは、取得条件と並び順を決めたQuery Builder/Eloquentへpaginateを指定し、結果をBladeへ渡して表示します。元の手順にある引用符の欠落を訂正し、同じ日時の行やJOIN、SQL本数、リンクの引継ぎも分けて確認します。

絞込み、一意な並び順、ページ分割とリンクの確認順

訂正版:取得条件と一意な並び順

以下はLaravel 13で確認した学習用クラスです。usersにはid/name/created_at/delete_flag、結合例には記載した独自列がある前提で、既存アプリへテーブルを作る指示ではありません。モデルのスコープを使わないQuery Builderの例なので、実装時は必要な権限・公開条件を自分で追加します。

<?php

namespace ContentExamples;

use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;

final class PageQueries
{
    public static function users(ConnectionInterface $db): Builder
    {
        return $db->table('users')
            ->select('id', 'name', 'created_at')
            ->where('delete_flag', 0)
            ->orderByDesc('created_at')
            ->orderByDesc('id');
    }

    public static function tweets(ConnectionInterface $db, string $since): Builder
    {
        return $db->table('ret_t_tweet')
            ->join('ret_m_user', 'ret_t_tweet.user_id', '=', 'ret_m_user.id')
            ->where('ret_m_user.delete_flag', 0)
            ->where('ret_t_tweet.created_at_shaped', '>=', $since)
            ->select(
                'ret_t_tweet.id as tweet_id',
                'ret_t_tweet.tweet_url',
                'ret_t_tweet.text',
                'ret_t_tweet.created_at_shaped',
                'ret_m_user.id as user_id',
                'ret_m_user.name as user_name'
            )
            ->orderByDesc('ret_t_tweet.created_at_shaped')
            ->orderByDesc('ret_t_tweet.id');
    }
}

クラスをautoloadできる位置へ配置するか読み込み、コントローラー内では次のようにページャーを作ります。名前空間・接続名・ビュー名は自分の構成へ合わせます。ここで件数は固定し、入力値で無制限に大きくしません。

$users = \ContentExamples\PageQueries::users(\Illuminate\Support\Facades\DB::connection())
    ->paginate(10);
return view('users.index', ['users' => $users]);

Bladeで一覧とリンクを表示する

次はresources/views/users/index.blade.phpに相当するビュー例です。ユーザー名は通常の二重波括弧でエスケープし、空結果も表示します。linksのCSSは使用するTailwind/Bootstrap等のページャービューに合わせます。

<ul>
    @forelse ($users as $user)
        <li>{{ $user->name }}</li>
    @empty
        <li>該当するユーザーはいません。</li>
    @endforelse
</ul>
{{ $users->links() }}

方式・検索条件・ページ境界の選び方

方式使い分け
paginate総件数・ページ番号が必要な一覧。通常は件数取得と一覧取得を行うが、空結果・指定total・関連取得等で本数は変わる
simplePaginate総件数を出さない前後移動。次の行の有無を調べるため指定件数より1行多く取得する
cursorPaginate一意なキーの位置から続きを取得。ページ番号での任意移動とは異なり、NULL不可等の条件を公式資料で確認する

日時とidの順序を固定しても、ページ移動中の追加・削除によるOFFSETの位置ずれは防げません。 検証では先頭への追加後に2ページ目の行がずれることも確認しました。カーソル方式も、順序列の更新や削除を含む完全なスナップショットの保証ではありません。

  • 検索条件を変えたらpageを1へ戻す。リンクへ残すのは検証し、実際にWHEREへ適用した許可キーだけにする。
  • withQueryStringは全クエリを引継ぐため、トークンや個人情報を含むURLでは安易に使わない。appendsも渡したキーを検証してくれる機能ではない。
  • 同じ画面に複数のページャーがあるなら、paginateの第3引数でusers_page等の別名を指定する。ページ指定の読み取りとフォームのhidden値も合わせる。
  • 遅い場合は総件数・一覧・関連取得を分けて測る。get後のCollectionをそのままDBクエリと同じものとして扱わず、深いOFFSETをsimplePaginateだけで解消できるとは考えない。

公式: Laravel Pagination。SQL条件はQuery Builder、実際の本数はSQLログの確認方法へ進めます。以下は元のコード表を保持した記録で、上の訂正と区別してください。

この記事の要点
  • Laravel のページング (Pagination)
  • サーバ: ->paginate(10) でクエリ末尾に追加 — 10 件ずつ
  • ビュー: {{ $users->links() }} でページャ HTML を出力
  • simplePaginate もある(次/前のみで全件カウントしない、軽量)

 

サーバー側の処理

次の黒い表は旧コードです。 orderByのcreated_atの閉じ引用符が欠けており、そのままではPHPの構文エラーになります。訂正はorderBy('created_at', 'DESC')です。同じ日時の行に備え、一意なidも並び順へ追加してください。Usersというモデル名もアプリに実在するものへ合わせます。

$users = Users::where('delete_flag', 0)
                ->orderBy('created_at, 'DESC')
               
->paginate(10);

取得条件・一意な順序を定義してからpaginateを呼びます。ページ番号が範囲外なら空結果になる場合があり、自動的に404へ変わるとは限りません。

引数には1ページに表示する件数を定義。

取得結果をBladeテンプレート側に渡す。

 

クライアント側の処理

{{ $users->links() }}

Bladeテンプレート側は上記の定義だけでページング用のリンクが自動生成される。(page=n のパラメータが自動で付与される)

以下の旧appends例は$idが定義され、検索条件として適用済みの場合の断片です。appendsはリンクへ値を加えるだけで、SQLのWHEREや入力検証・閲覧権限を追加しません。秘密値や不要なクエリをリンクへ転送しないでください。

{{ $users->appends(['id' => $id])->links() }}

 

サーバー側のもう少し複雑なSQLの実行例

次はmysql2接続、独自テーブルと$todayを使う旧断片です。接続設定・時刻の基準・列の型が必要で、このまま動く汎用例ではありません。ORDER BYがないのでページの並びは安定しません。上の訂正版ではtweet側の日時と主キーを指定し、user_idとtweet_idを分けています。JOINが1対多なら件数は親の数ではなく結合後の行数になる点も確認してください。

$results = DB::connection('mysql2')
                ->table('ret_t_tweet')
                ->join('ret_m_user', 'ret_t_tweet.user_id', '=', 'ret_m_user.id')
                ->where('ret_m_user.delete_flag', 0)
                ->where('ret_t_tweet.created_at_shaped', ">=", $today)
                ->select('ret_t_tweet.tweet_url as tweet_url'
                        , 'ret_t_tweet.text as text'
                        , 'ret_t_tweet.created_at_shaped as created_at_shaped'
                        , 'ret_m_user.id as user_id'
                        , 'ret_m_user.name as user_name'
                        )
                ->paginate(10);

 

子ページ

子ページはありません

同階層のページ
  1. インストールと設定
  2. クイックスタート & チュートリアル(初心者向け)
  3. クイックスタート & チュートリアル(中級者向け)
  4. ルーティング
  5. Bladeテンプレート(ビュー/レイアウト)
  6. コントローラー
  7. マイグレーションとテーブル定義
  8. データベースの設定
  9. Eloquentモデル (ORM)
  10. SQLとクエリビルダー
  11. バリデーション
  12. .envファイルの設定値へのアクセス
  13. 動作環境による分岐処理
  14. configフォルダ配下の設定値へのアクセス
  15. assetヘルパーを利用したpublicフォルダへのアクセス
  16. storageフォルダへのアクセス
  17. アプリケーション名の変更
  18. メンテナンス
  19. ログイン画面(認証システム)の作成
  20. ログインの必須化
  21. ログインユーザー情報の取得
  22. ルートの認証化
  23. 本番サーバーへのデプロイ方法
  24. 多言語化
  25. csrf_field
  26. ファイルのダウンロード
  27. CSVのアップロードおよび読み込み(maatwebsite/excel)
  28. ページタイトルの設定
  29. コマンド一覧
  30. エラー一覧
  31. SQLの実行ログ出力方法
  32. キャッシュのクリア
  33. Selectの結果の最初もしくは最後に任意の値を追加する方法
  34. ajaxでPOST通信する際の注意点
  35. ソーシャルログインの実装
  36. セッション情報の確認
  37. ログイン、ユーザー登録、パスワードリセット後のリダイレクト先の変更方法
  38. redirectやreturn viewにメッセージを付与する方法
  39. クッキー(cookie)の設定と取得
  40. クラスの再読み込み
  41. csrfの有効時間を変更する方法
  42. ViewComposerを用いてviewに共通の値を付与する方法
  43. View::shareを用いて共通の値を各ビューに渡す方法
  44. ミドルウェアを用いた処理の共通化
  45. Middleware内でAuth::check()などを使用する方法
  46. Controller以外でリダイレクトする方法
  47. セッションの値の取得/保存/更新/削除
  48. $requestの値を変更する方法
  49. 常時SSL化
  50. ページング(ページネーション)をする方法
  51. vue.jsとの連携
  52. Vue.jsと連携するSPA実行環境構築
  53. .envの値をvue.jsで参照する方法
  54. vue.jsを本番環境にリリースする方法
  55. could not find driver(Windows, MySQL編)

最近更新/作成されたページ