クエリの減らし方(withメソッド)
インターン生の望月です。
SDB Tech Blog Advent Calendar 2023の7日目になります。
Laravelを使い始めてまだ数ヶ月の初心者ですが、少し試行錯誤した内容だったので記事にします。
0. 前提として
今回私が使用している環境は、
①Laravel ver 8.x
②APP_DEBUGがtrue
となります。
※ Laravelのversionや設定の次第では、そのまま適用できるわけではないことに注意してください。
ドキュメント:Laravel 8.x Eloquent: リレーション
1. 事前知識
クエリとは、「データベースに対してデータを操作するために発行するSQL文の数」
動的プロパティとは、「リレーションにアクセスし、取得したレコードをプロパティとしたもの」
今回使用するメソッド
with('relation') // relationで定義しているモデルを事前にまとめて取得する
※'relation' // リレーションを定義しているfunctionの名前
※他にもクエリを減らす方法があるので、「Eager Loading」と調べてみてください。
クエリを減らす上で、意識することは
必要なものを、事前に全てまとめて、読み込むことです。
2. 現在のクエリの確認
これは、例として用意したLaravelのdebugbarの画像になります。これのQueriesと書かれている場所を注目してもらうと、数字が書かれています(上の画像だと43)。
これが現在の通信に対するクエリの数になります。
このクエリの発行数が多いほど、ページのロードに時間がかかったり、サーバーに対する負荷が大きくなったりするので、極力減らすようにしましょう。
3. コードの確認
2.で現在のページでのクエリの発行数を確認できました。ここからはクエリの発生源を追ってみます。
debugbarのQueriesのタブを開くと、下の方に1つ1つのクエリに関する情報が表示されます。赤くなっているのは、それぞれのクエリでかかった時間を表しています。
このクエリの帯をクリックしてみると、特定のクエリでのBacktraceが表示されます。
このBacktraceを確認してみると、クエリを発行している場所が分かります。
クエリの発行元を特定できたので、実際にそのファイルの行を見に行くと、どの部分で発生しているかを突き止めることができます。
クエリを発行する状況を考えてみると、
データベースからレコードを取得して、そのレコードを元に別のテーブルから関連するレコードを取得する
このようなデータベースからデータを取得する状況になります(クエリが何かを考えるとそうなりますね)。
特定の条件でレコードを取得している部分を探すようにすると、どこでクエリを発行しているのかを見つけやすいです。
4. コードの変更
ここまでで、クエリの発行元を特定できました。
それでは、実際にクエリを減らすためにコードを書き換えていきましょう。
そもそもなぜこのままだと、クエリが多くなってしまうのかについて簡単に説明します。
まずデータベースからデータを読み込む際に、読み込み方が2つあります。
Lazy LoadingとEager Loadingになります。
Lazy Loading:必要になった段階で読み込みを行う
Eager Loading:最初に全ての読み込みを行う
このままだと分かりづらいので、図にしてみます。
Lazy Loadingでは、必要になってから適宜データベースからレコードを取得する
Eager Loadingでは、一番最初に全てのレコードを取得する
このように考えると分かりやすいかもしれないです。
これで、Lazy LoadingとEager Loadingがどんなものかを掴めたと思います。
その上で、どちらの読み込みを使うことでクエリを減らせるのかを考えると、
Eager Loadingになります。
では、Eager Loadingをするにはどうするかというと、今回はwithメソッドを使ってクエリを減らしていきます。
今回はUserモデルからPostモデルへのリレーションを例に使って考えます。
UserモデルとPostモデルは一対多の関係になっているもので考えます。
public function getPost()
{
return $this->hasMany(Post::class);
}
この、リレーションを定義しているfunctionをモデルからチェーン(->)で繋ぐことで、現在のモデルに関連している他のモデルのレコードだけを取得できます。
$users = User::all();
foreach($users as $user){
$post = $user->getPost()->get()
}
ただ、このままだと、クエリの発行数が増えてしまいます(N+1問題を引き起こします)。
そこで何をするのかというと、withメソッドを使います。
モデルインスタンスを取得する前にwithメソッドを使って必要なデータを取得しましょう。
$users = User::with('getPost')->get();
このままだと取得したPostモデルのレコードへアクセスできないと思うかもしれませんが、ご安心ください。
withメソッドを使って取得したレコードは、動的プロパティとして格納されています。
これはどういうことかというと、以下の図のような形で取得されています。
このようにすることで、$usersの各レコードに対応しているレコードだけを取得できます。
取得した動的プロパティの値にアクセスしたいときは、以下のようにチェーンすることでアクセスできます。
○ $user->getPost
× $user->getPost()
※リレーションのfunction名の後に()をつけないようにしてください
5. 変更後のクエリの確認
では、早速変更したコードでのクエリの発行数を確認してみましょう。
確認した結果、クエリの数が減っていれば成功です。この調子で他のクエリに対しても3,4を実行してみましょう。
減っていない場合は、back traceの内容が変わっているかを確認してみましょう。
back traceに表示されているファイルまたは行数が変わっていれば、そこで新しくデータにアクセスしていることになるので、そこより前の段階でwithメソッドを使って事前ロードしましょう。
※withメソッドを使う場合は、モデルインスタンスの状態だとエラーになってしまいます(モデルインスタンスの場合はloadメソッドを使いましょう)。
6. 最後に
以上がクエリの減らし方になります。
クエリの発行数はコーディングルールをしっかり定めることで、抑えることができるようになります。
さらに、サーバーの状況次第では、クエリを最低限まで減らさない方がいい場合もあります。この塩梅に関しては、アプリケーションごとに変わってくるので、自分のアプリケーションにおける最適なバランスを探してみください。
ここまで読んでくださりありがとうございました。
Discussion