【Laravel】書籍管理システムを作る(4)一覧・詳細画面の実装とN+1問題の対策(Eager Loading)

Laravel
記事内に広告が含まれています。

Laravelによる書籍管理システムの作成、第4回です。
第3回目でデータベースにレコードを挿入したので、今回はそのレコードを表示する、一覧画面、詳細画面の作成手順を追って行きたいと思います。

  1. 1. コントローラー (処理ロジック) の作成
    1. 1-1. コントローラーの作成コマンド
    2. 1-2. BookController.php の編集
    3. 1-3. コントローラーは「データ集め」と「行き先の決定」に専念する
      1. コントローラーは何を行わせるところか?
      2. 「Route Model Binding」の凄さを体感
    4. 1-4. 「Eager Loading (‘with’)」について
      1. 1-4-1. Eager Loading がない場合(N+1問題)
        1. コントローラーのコード(悪い例):
        2. ビューのコード:
        3. 何が起きているのか?
      2. 1-4-2. Eager Loading を使った場合
        1. コントローラーのコード(良い例):
        2. 何が起きているのか?
      3. 1-4-3. 例え話
        1. Eager Loadingなし(N+1):
        2. Eager Loadingあり:
      4. 1-4-4. Eager Loading まとめ
      5. 1-4-5. Eager Loading 応用
  2. 2. ルーティング(URL定義)の設定
    1. 2-1. routes/web.php にルーティングを設定する
    2. 2-2. ルーティングで窓口と行き先をハッキリさせる
        1. なぜこれが必要なのか?:
        2. ここが重要!:
    3. 2-3. ルート設定の書き方
      1. 2-3-1. HTTPメソッドの設定
      2. 2-3-2. URLの設定
      3. 2-3-3. コントローラーとメソッドの設定
      4. 2-3-4. ルート名の設定
    4. 2-4. ミドルウェアを使用してログイン認証の判定を行う
      1. 2-4-1. ログイン認証済みの場合のみ表示する
      2. 2-4-2. ビューの表記方法
  3. 3. ビュー (HTML) の作成
    1. 3-1. 一覧画面ビューの作成
    2. 3-2. 詳細画面ビューの作成
    3. 3-3. ビューはデザインとデータの流し込み
        1. なぜこれが必要なのか?:
        2. Blade(ブレード)の便利さ:
    4. 3-4. リレーションとエラー回避
      1. 3-4-1. Null合体演算子
        1. $book->category(リレーションの呼び出し):
        2. ->category_name(プロパティへのアクセス):
        3. ?? 'N/A'(合体演算子 / Null合体演算子):
      2. 3-4-2. 三項演算子との比較
        1. 従来の三項演算子 (?:) で書いた場合:
        2. Null合体演算子 (??) で書いた場合
      3. 3-4-3. なぜ「Null合体演算子」を使うのか?
        1. ① コードが短くなる
        2. ② 「変数がそもそも存在しない」場合でもエラーにならない
    5. 3-5. Breezeで作成されたメニューの修正
      1. 3-5-1. ユーザー名が表示されるところ
      2. 3-5-2. ダッシュボードページ
    6. 3-6. Tailwind CSS
  4. 4. 動作確認
    1. 4-1. ログイン済みページ(ダッシュボード)
    2. 4-2. 書籍一覧ページ
    3. 4-3. 書籍詳細ページ
  5. 関連するポスト

1. コントローラー (処理ロジック) の作成

1-1. コントローラーの作成コマンド

ではまず Ubuntu のターミナルから以下のコマンドを実行し、BookController を作成します。

上記コマンドを実行すると、app/Http/Controllers/BookController.php が作成されます。

1-2. BookController.php の編集

作成したコントローラーに index メソッド(一覧)と show メソッド(詳細)を定義します。

特に show メソッドでは、ルート定義の {book} パラメーターと型ヒント(Book $book)を合わせることで、Route Model Binding が適用され、ID検索と404エラー処理が自動で実行されます。

編集ファイル: app/Http/Controllers/BookController.php

 

1-3. コントローラーは「データ集め」と「行き先の決定」に専念する

コントローラーは何を行わせるところか?

データベースから必要な本を探してきたり、それを1ページ何件表示にするかといった「計算や判断(ロジック)」を行う場所です。

「Route Model Binding」の凄さを体感

わたしが以前使用していたCakePHP(2系統)では、詳細画面において1件のデータを取得するのにも find文 でレコードを取得していました。

Laravel でも 通常なら Book::find($id) と書いてデータを探すコードが必要ですが、メソッドの引数に Book $book と型を書くだけで、Laravel が裏側で自動的にデータベースからその 1件 を探し出してくれます。もし存在しなければ自動で 404画面 を出してくれるので、コードが非常にスッキリします。

1-4. 「Eager Loading (‘with’)」について

Book::with(['category', 'authors']) と書くのは、 N+1問題  という「データベースへのアクセス回数が増えすぎて重くなる現象」を防ぐためです。最初にまとめて関連データを取ってくるのが、上級者な書き方です。

ここは重要だと思いますので、もう少し深堀りします。

 Eager Loading(イーガーローディング) は、Laravelでデータベースからデータを取得する際の「効率化のテクニック」です。

これを理解するために、まずは問題点である 「N+1問題」 から紐解いていきましょう。

1-4-1. Eager Loading がない場合(N+1問題)

例えば、20冊の書籍一覧を表示し、それぞれの「カテゴリ名」も表示したいとします。

コントローラーのコード(悪い例):

ビューのコード:

何が起きているのか?
  1. まず、本を全件取得するために <1回> SQLを実行します。
  2. ループの中で、各本のカテゴリ名を表示しようとするたびに、< 本の数だけ(N回)> 追加でSQLを実行します。

合計で 1 + 20 = 21回 もデータベースに問い合わせをすることになります。
これがデータの件数が100件、1,000件と増えると、動作が激重になってしまいます。これが  N+1問題 です。

1-4-2. Eager Loading を使った場合

Eager Loadingは、日本語で「あらかじめ(一括で)読み込む」という意味です。

コントローラーのコード(良い例):

何が起きているのか?
  1. 本の全件取得。
  2. その本たちに関連するカテゴリIDをLaravelがまとめ、WHERE IN (1, 2, 3...) というSQLを<1回だけ>実行して、必要なカテゴリをすべて一気に取得します。

これなら、本が100万冊あってもSQLは<合計2回>で済みます。

1-4-3. 例え話

ピンとこない人がいるかと思いますので、スーパーでの買い物というシチュエーションに例えてみましょう。

Eager Loadingなし(N+1):

「カレーの材料を買ってきて」と言われ、スーパーへ行く。

  1. スーパーへ行き、ジャガイモを買って帰る。
  2.  「あ、人参も必要だった」と気づき、またスーパーへ行って帰る。
  3.  「あ、玉ねぎも…」とまたスーパーへ行く。 (往復回数が多くてヘトヘトになりますね)
Eager Loadingあり:

「カレーの材料(ジャガイモ、人参、玉ねぎ)を買ってきて」と言われ、メモを持ってスーパーへ行く。

1. スーパーへ行き、全ての材料を一度にカゴに入れて帰る。 (1回の往復で済むので非常に効率的!)

うーん、これでいいのか?(苦笑)
より理解しやすいよう、表にもまとめてみます。

状態 N+1問題(Eager Loadingなし) Eager Loading(あり)
買い物の動き 必要なものを思い出すたびにスーパーへ行く メモを持って一度の訪問ですべて揃える
SQLの実行 「本の数(N)」+「最初の1回」で合計N+1回実行される WHERE IN句などを使い、合計2回程度の実行で済む
サイトの速度 データ件数が増えると、表示が非常に重くなる 大量のデータがあっても、負荷を最小限に抑えられ高速
Laravelの記述 $books = Book::all();  $books = Book::with('category')->get(); 

実際のコードでは with() が買い物メモの役割を果たします。

1-4-4. Eager Loading まとめ

  • with() を使う理由:データベースへの「往復回数」を最小限にして、サイトの表示速度を速くするため。
  • いつ使う?:一覧画面などで、ループ(@foreach)の中でリレーション($book->category など)を使うときは必ず検討。

1-4-5. Eager Loading 応用

一覧表示のコードで以下のように書いたのは、著者やタグも一気に取得するためです。

これで、どれだけ情報量が多い一覧画面でも、データベースへの負荷を最小限に抑えることができます。
うーん、便利!

 

2. ルーティング(URL定義)の設定

次に、アプリケーションがどのURLでどの処理を実行するかを定義します。

2-1. routes/web.php にルーティングを設定する

編集ファイル:routes/web.php

 

2-2. ルーティングで窓口と行き先をハッキリさせる

なぜこれが必要なのか?:

Laravelは、URLごとに「どのコントローラーの、どのメソッド」を動かすかをあらかじめ決めておく必要があります。
これを定義しないと、ブラウザでアクセスしても 404 Not Found となります。

ここが重要!:

{book} という書き方は「ルートパラメーター」と呼ばれます。
例えば /books/1 でアクセスされたとき、この 1 という数字を自動的に読み取って、コントローラーに渡してくれる便利な仕組みです。

2-3. ルート設定の書き方

ルート設定の書き方は次のとおりです。

Route::HTTPメソッド(‘URL’, [コントローラ::class, ‘メソッド’])->name(‘ルート名’);

実際の routes/web.php には、次のように書かれています。(次回以降作成予定のものも含まれています)

この記述について、詳しく見ていきます。

2-3-1. HTTPメソッドの設定

最初の 「Route::get」 の ‘get’ や ‘post’ は HTTPメソッド  です。
HTTPメソッドとは、ブラウザからWebサーバに要求の種類を伝える文字列です。要求によって次の4つのメソッドを使用します。

HTTPメソッドの種類
HTTPDメソッド名 実行するアクション
get ページを表示
post データを保存
put または patch データの更新
delete データの削除

2-3-2. URLの設定

ルート設定の 「/books」 と書かれた部分には、ドメイン以下のURL を記述します。
URL にパラメータを含む場合には、波括弧を使って記述します。

詳細画面のルート設定に 「/books/{book}」 と記述しています。上の ここが重要! の段でも触れましたが、これは book パラメータを ルートパラメーターとして show メソッドへ渡しているところとなります。

詳細画面のほか、編集画面、編集処理、削除処理のメソッドに対してパラメータを渡し、どのレコードに対して処理を行うのかを指定しています。

2-3-3. コントローラーとメソッドの設定

次の 「BookController::class, ‘index’」 は、このURLをリクエストされたときに処理を行うコントローラー名とメソッド名を記述します。
この場合は Bookコントローラー の indexメソッド ということになります。

なお、ルート設定ファイルの上部には、コントローラーファイルの場所を示す use 宣言を記述しておく必要があります。

この部分ですね。忘れがちなので、注意してください。

2-3-4. ルート名の設定

一覧画面表示用のルート設定の最後には 「->name(‘books.index’)」 、詳細画面用のルート設定の最後には 「->name(‘books.show’)」とありますが、これは このルート設定の名前 を示しています。

ルート名は、 プロジェクト内でこのルート設定を呼び出すときに使われる名前 となります。

例えば、詳細画面のビューファイル(show.blade.php)上に 「一覧画面へ」 をクリックすると一覧画面が表示されるリンクを作りたい場合には、ルート名を使用すると次のようにシンプルに記述できます。

 

2-4. ミドルウェアを使用してログイン認証の判定を行う

2-4-1. ログイン認証済みの場合のみ表示する

一覧画面、詳細画面は認証していない状態でも閲覧可能ですが、作成、編集、削除するには認証が必要、という感じにするため、認証用のミドルウェアにて判定させるようにします。

なお、各画面は次回以降で作成予定ですので、現状はコメントアウトしています。

2-4-2. ビューの表記方法

ビューは次のステップで作成しますが、判定部分のコードを先出しすると以下のようになります。
@auth@endauth のタグで囲んだ部分はログインしているユーザーにしか表示されません。
なるほどですね。

 

3. ビュー (HTML) の作成

取得したデータを表示するためのビューページBladeを作成します。
なお、モデルファイルやコントローラーなどはコマンドを実行して作成しましたが、ビューに関してはディレクトリ、ファイルともに手動で作成するのが一般的なようです。

ビューの作成にもコマンドがあるものだと思っていましたが、ここだけは手動で行うとはなんだか不思議ですね。

3-1. 一覧画面ビューの作成

データをテーブル形式で表示し、詳細ページへのリンクを設置します。

編集ファイル: resources/views/books/index.blade.php

一覧画面では、取得したデータを foreach でグルグル回すのが基本です。
ページ送り(ページネーション)も非常に楽に記述できるのが Laravel の良いところですよね。

3-2. 詳細画面ビューの作成

特定の書籍に関する全ての情報を表示します。

編集ファイル: resources/views/books/show.blade.php

「タグ」を表示する箇所で @forelseディレクティブ を使用しています。

@forelsedhディレクティブ は、@foreachディレクティブ と似て、ループを処理するのですが、「$book->tags」が空の際には @empty 内を処理する便利なディレクティブですので、頭の片隅にでも置いておきましょう。

3-3. ビューはデザインとデータの流し込み

なぜこれが必要なのか?:

コントローラーが用意したデータは、そのままではただの「配列(データの塊)」です。
これを人間に見やすいHTMLに加工するのがビューの役割です。

Blade(ブレード)の便利さ:

@foreach{{ $book->title }} といった短い記法で、PHPのコードをHTMLの中に埋め込めます。
また、route('books.show', $book) と書けば、URLが将来変わっても自動で正しいリンクを作ってくれます。

3-4. リレーションとエラー回避

3-4-1. Null合体演算子

一覧画面のカテゴリーを表示する部分ですが、以下のように書かれています。

詳細画面でも同様に、以下のようになっています。

三項演算子のような書き方ですが、これは 「Null合体演算子(Null Coalescing Operator)」 と呼ばれる、PHP7.0で導入された演算子で、「??」を使い、 isset() の代用として使える機能で、平たく言うと、「三項演算子の進化版(あるいは短縮版)」とも言えるものです。

$book->category(リレーションの呼び出し):

これは「Bookモデルから、紐付いているCategoryモデルを呼び出す」という命令です。

  • 仕組み: Book.php モデル内に書いた public function category() メソッドを実行しています。
  • 結果: データベースの m_categories テーブルから、その本の category_id と一致するレコードを1件取ってきます。
->category_name(プロパティへのアクセス):

取ってきたカテゴリ情報のレコードの中から、「カテゴリ名」が保存されているカラムの値 を取り出します。
例えば、その本のカテゴリが「技術書」であれば、ここで「技術書」という文字列が取得できます。

?? 'N/A'(合体演算子 / Null合体演算子):

ここが重要なポイント。「もしデータがなかったら(Nullだったら)右側の文字を出してね」 というPHPのいわば安全装置です。

もし、データベース上の category_id が空っぽ(NULL)だったり、紐づくカテゴリが削除されて存在しなかった場合、$book->categorynull になります。 null に対して ->category_name を実行しようとすると、「Attempt to read property “category_name” on null(中身が空っぽなのに名前を読もうとしたよ!)」という 致命的なエラー が発生し、画面が真っ白になってしまいます。

そこで解決策として 「??」 を使うことで、「カテゴリデータがあればその名前を、なければ 『N/A(該当なし)』 という文字を表示して、エラーを回避する」という処理を、1行でスマートに書くことができるという訳です。

3-4-2. 三項演算子との比較

せっかくなので、三項演算子との違いをまとめてみます。

従来の三項演算子 (?:) で書いた場合:

上記の記述だと、「もしデータが存在するか(isset)?」を確認し、真なら左、偽なら右…という3つの要素を書く必要があります。

Null合体演算子 (??) で書いた場合

すでに例で出ていますが、再度。

「左側が null でなければそれを使い、null なら右側を使う」という動きをします。

 

3-4-3. なぜ「Null合体演算子」を使うのか?

① コードが短くなる

三項演算子だと「データの存在確認」をしてから「同じデータをもう一度書く」必要がありますが、Null合体演算子なら1回書くだけで済みます。

② 「変数がそもそも存在しない」場合でもエラーにならない

これが最大の違いです。

  • 三項演算子:もし $book->category という変数が定義されていなかった場合、いきなり使おうとすると「そんな変数ないよ!」というエラー(Notice)が出ることがあります。
  • Null合体演算子:左側の変数が定義されていなくても、エラーを出さずにスマートに右側の 'N/A' を返してくれます。

以上をまとめると、使い分けのイメージとしては、

  • 「AかBか」という条件分岐をしたいなら ➔  三項演算子  ($age >= 20 ? '大人' : '子供')
  • 「データがない時の予備(デフォルト値)」 を決めたいなら ➔  Null合体演算子  ($name ?? '名無しさん')

今回の「カテゴリ名を表示する」というケースは、まさに「データがない時の予備」を表示するパターンなので、「??」 が最適解となります。
なるほどですね。

3-5. Breezeで作成されたメニューの修正

3-5-1. ユーザー名が表示されるところ

この連載の最初にBreezeにて認証の仕組みを作成しました。認証済みの場合にユーザー名が表示される箇所があります。
しかしながら、未ログインでも表示させるページ(書籍一覧、書籍詳細)がある仕様ですので、未認証の場合にはユーザー名として「名無しさん」が表示されるよう、少し細工をします。

編集ファイル:resources/views/layouts/navigation.blade.php

↑ 25行目付近に、右上にユーザー名を表示する記述があるので、 @else を追記して「名無しさん」を表示するようにします。

↑ 同じく80行目付近に、スマホ用のメニュー内にユーザー名を表示している箇所があるので、そこにも @else にて「名無しさん」を表示するようにします。

3-5-2. ダッシュボードページ

ログインした場合に表示されるダッシュボードページに、書籍一覧へのリンクを追加します。ひとまず「ログイン成功」のブロックをコピペして作成。

リンク先にはルーティングのときに name で指定した {{ route('books.index') }} を使うと楽にリンクできます。

編集ファイル:resources/views/dashboard.blade.php

 

3-6. Tailwind CSS

最後にCSSのフレームワークについて。

ビューの各タグ内のクラス指定ですが、これはCSSのフレームワークである 「Tailwind CSS」 の記述方法となっています。Laravel 8 以前では 「Bootstrap」 がデフォルトのCSSフレームワークでしたが、現在はTailwind CSSが標準で使用されています。

CSSフレームワークについてはまあ、説明しなくても大丈夫かなとは思いますが、言ってしまえば効率よくスタイルを設定するための道具のようなものです。

各クラスの意味を知るには、Tailwind CSS の公式マニュアルが役立ちます。

https:tailwindcss.com

公式サイトの検索ボックスにクラス名を入れると、解説ページの候補が出てきます。解説ページの上部には、クラス名がどんなスタイルを意味しているかが書かれています。一見あれ。

考え方、理解の方法はこちらのサイト様を参照してください。

 

4. 動作確認

ブラウザで以下のURLにアクセスし、画面が表示されるか確認してください。

1. ダッシュボード:  http://localhost/dashboard
2. 一覧画面:  http://localhost/books
3. 詳細画面:  http://localhost/books/1

 

4-1. ログイン済みページ(ダッシュボード)

ダッシュボード

 

4-2. 書籍一覧ページ

書籍一覧

 

4-3. 書籍詳細ページ

書籍詳細

 

以上、駆け足でしたが、書籍データの表示機能が完成しました。

 

関連するポスト

Laravel
スポンサーリンク
シェアする
toogieをフォローする
タイトルとURLをコピーしました