【Laravel】掲示板を作成する(7)クエリーの調整(Eagerロード)、キーワード検索機能
Laravelによる掲示板の作成、第7回です。
今回は『N+1』問題を解決するEagerロードというクエリーの調整方法と、一覧画面にキーワード検索機能を追加してみたいと思います。
(第1回)1.各種設定
(第1回)2.マイグレーションでDBを作成する
(第2回)3.Eloquent機能を使いモデルのリレーションを設定する
(第2回)4.LaravelのSeed機能とFakerを使ってDBにテストデータを登録する
(第3回)5.一覧画面の作成
(第3回)6.詳細画面の作成
(第4回)7.新規投稿機能の作成
(第4回)8.コメント投稿機能の作成
(第5回)9.投稿編集機能の作成
(第5回)10.投稿の物理削除機能の作成
(第6回)11.投稿・編集画面のカテゴリーをプルダウンメニュー化する
(第6回)12.特定カテゴリーの記事を検索して表示する
(今回)13.クエリーの調整(Eagerロード)
(今回)14.キーワード検索機能
13.クエリーの調整(Eagerロード)
有名な『N+1問題』を解決する方法がLaravelには備わっているということなので、試してみます。
まずは実行されているクエリがわかるように、SQLの実行ログを取ってみます。
サービスプロバイダ登録
app/Providers/AppServiceProvider.php
を開いて、「boot」メソッドを以下のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?php namespace App\Providers; use Illuminate\Support\Facades\Schema; // ←★追記 use Illuminate\Support\ServiceProvider; use DB; // ←★追記 class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { // } /** * Bootstrap any application services. * * @return void */ public function boot() { // 本番環境以外だった場合、SQLログを出力する if (config('app.env') !== 'production') { DB::listen(function ($query) { \Log::info("Query Time:{$query->time}s] $query->sql"); }); } } } |
上記コードを記述した状態で一覧ページをにアクセスすると、下記のようなログファイルが取得できます。
ログファイルは storage\logs
以下に作成されます。
ログ内容
タイムスタンプ等は今回端折っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
select `name`, `id` from `categories` order by `id` asc select count(*) as aggregate from `posts` select * from `posts` order by `created_at` desc limit 10 offset 0 select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null select * from `categories` where `categories`.`id` = ? limit 1 select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null |
同じタイムスタンプで合計23回のクエリが発行されていました。
一覧画面には「投稿のリスト」と「投稿に紐づくコメントの合計数」を表示しているため、取得した投稿の回数(10回)分コメント数をカウントするクエリが発行されてしまっています。
具体的に書くと以下のSQLでリストを取得し、
1 |
select * from `posts` order by `created_at` desc limit 10 offset 0 |
次のSQLでコメント数を取得しています。
このコメント取得SQLが10回走っています。
1 |
select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null |
さらにカテゴリー名を取得するのにも同じようなクエリが10回発行されています。
1 |
select * from `categories` where `categories`.`id` = ? limit 1 |
このような問題を『n+1』問題と言いまずが、Laravelでは一覧取得時に with
メソッドを利用することで解決することができるとのこと。
with() を使ったEagerローディング
PostsControllerのindexメソッドで投稿のリストを取得している処理を、以下のように変更します。
編集ファイル:app\Http\Controllers\PostsController.php
1 2 3 4 5 6 7 8 9 10 |
$posts = Post::orderBy('created_at', 'desc') ->categoryAt($category_id) ->paginate(10); ↓↓↓↓↓ 以下に変更 ↓↓↓↓↓ $posts = Post::with('comments') // ←★これ ->orderBy('created_at', 'desc') ->categoryAt($category_id) ->paginate(10); |
なお、withの中に記述している comments
が複数形なのは、post対commentは「1対多」としているからです。
この状態で一覧にアクセスしてログを確認すると。。。おお、コメントを取得していたSQLがINを使ったものになって、回数は大幅に減っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
select `name`, `id` from `categories` order by `id` asc select count(*) as aggregate from `posts` select * from `posts` order by `created_at` desc limit 10 offset 0 select * from `comments` where `comments`.`post_id` in (2, 8, 10, 12, 15, 17, 19, 28, 32, 52) select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 select * from `categories` where `categories`.`id` = ? limit 1 |
ただし カテゴリーの処理がまだそのままですので、さらに先程のwithにcategoryを設定してみます。
1 2 3 4 |
$posts = Post::with(['comments', 'category']) // ←★"category"を追加 ->orderBy('created_at', 'desc') ->categoryAt($category_id) ->paginate(10); |
このcategoryは単数形で設定。postから見た場合のcategoryは1つという設定だからです。
最終的なログ
1 2 3 4 5 |
select `name`, `id` from `categories` order by `id` asc select count(*) as aggregate from `posts` select * from `posts` order by `created_at` desc limit 10 offset 0 select * from `comments` where `comments`.`post_id` in (2, 8, 10, 12, 15, 17, 19, 28, 32, 52) select * from `categories` where `categories`.`id` in (1, 2, 3) |
最終的に、発行されるSQLは5個になりました。おお、すごい。
ログファイルの取得には以下のサイト様を参考にしました。
https://daiki-sekiguchi.com/2018/07/26/laravel-sql-log/
https://qiita.com/qwe001/items/96a83fadcfaeb3cd4a6e
14.キーワード検索機能
名前(投稿者名)や本文を対象にキーワード検索できるようにしてみます。
まずは「名前」を対象とした検索機能を作成してみます。
名前検索
検索にはカテゴリーでも使用したスコープを使います。
Postモデル編集
編集ファイル:app\Post.php
名前検索用のスコープを追加します。
1 2 3 4 5 6 7 8 9 10 |
/** * 「名前」検索スコープ */ public function scopeFuzzyName($query, $searchword) { if (empty($searchword)) { return; } return $query->where('name', 'like', "%{$searchword}%"); } |
Postsコントローラー編集
編集ファイル:app\Http\Controllers\PostsController.php
indexメソッドへ以下を追加します。
1 |
$searchword = $request->searchword; |
一覧取得する箇所のチェーンメソッドに先程のスコープを追加します。
1 2 3 4 5 |
$posts = Post::with(['comments', 'category']) ->orderBy('created_at', 'desc') ->categoryAt($category_id) ->fuzzyName($searchword) // ←★追加 ->paginate(10); |
検索ワードをviewに渡せるようにします。
1 2 3 4 5 6 |
return view('bbs.index', [ 'posts' => $posts, 'categories' => $categories, 'category_id' => $category_id, 'searchword' => $searchword // ←★追加 ]); |
ビューを編集
編集ファイル:resources\views\bbs\index.blade.php
検索フォームを任意の場所に追加します。
1 2 3 4 5 6 7 8 9 |
<!-- 検索フォーム --> <div class="mt-4 mb-4"> <form class="form-inline" method="GET" action="{{ route('bbs.index') }}"> <div class="form-group"> <input type="text" name="searchword" value="{{$searchword}}" class="form-control" placeholder="名前を入力してください"> </div> <input type="submit" value="検索" class="btn btn-info ml-2"> </form> </div> |
ページ送りに渡す引数を追加します。
1 2 3 4 5 6 |
<div class="d-flex justify-content-center mb-5"> {{ $posts->appends([ 'category_id' => $category_id, 'searchword' => $searchword // ←★追加 ])->links() }} </div> |
一覧画面キャプチャ(検索前)
一覧画面キャプチャ(検索実行後)
「田」(田園の田)という字を検索ワードにしてみました。
名前・投稿内容を『OR検索』する
次に、1つの検索ワードで名前と投稿内容をOR検索ができるようにしてみます。
「名前」もしくは「投稿内容」に『形』という検索ワードが含まれているものを表示するというイメージです。
Postモデル編集
編集ファイル:app\Post.php
名前・投稿内容検索用のスコープを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * 「名前・本文」検索スコープ */ public function scopeFuzzyNameMessage($query, $searchword) { if (empty($searchword)) { return; } return $query->where(function ($query) use($searchword) { $query->orWhere('name', 'like', "%{$searchword}%") ->orWhere('message', 'like', "%{$searchword}%"); }); } |
「use」を使ってクロージャへ $searchword 変数を渡すのがポイントです。
Postコントローラー編集
例によって、チェーンメソッドを名前・投稿内容検索用のスコープに変更します。
編集ファイル:app\Http\Controllers\PostsController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$posts = Post::with(['comments', 'category']) ->orderBy('created_at', 'desc') ->categoryAt($category_id) ->fuzzyName($searchword) ->paginate(10); ↓↓↓↓↓ 以下に変更 ↓↓↓↓↓ $posts = Post::with(['comments', 'category']) ->orderBy('created_at', 'desc') ->categoryAt($category_id) ->fuzzyNameMessage($searchword) // ←★変更 ->paginate(10); |
検索結果のキャプチャ
『形』という検索ワードでの結果です。
ID:19の名前には「形」が含まれていますが、本文には含まれていません。
逆にID:17番の名前には「形」は含まれていませんが、本文に含まれていました。
うまくいっているようですね。
ID:19の詳細キャプチャ
ID:17の詳細キャプチャ
今回はここまでとなります。