🎄

クエリの減らし方(withメソッド)

2023/12/07に公開

インターン生の望月です。

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 LoadingEager 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