ひでメモ

プログラムについて勉強したことを書きます。たぶん。

【Laravel】モデルのアクセサでリレーションを呼ぶと N+1 問題になるときの回避方法

ある日 Telescope 上で同じクエリが数十件発行されているリクエストを見つけて驚きました。
コードを追ってみるとどうやらモデルのアクセサでリレーションが呼ばれていることが原因のようでした。
そもそもそういうコードを書くな、ということなのでしょうがすでに書いてあるためどうやってクエリの大量発行が回避できるか検討してみました。

問題

モデルのアクセサでリレーションを呼び、appendsにそのアクセサを追加していると index 系のメソッドなどでモデルのインスタンスが大量に作られると N+1 問題が発生します。
モデル側のコードとしては以下のようなイメージです。

protected $appends = ['user_name'];

public function getUserNameAttribute()
{
      return $this->user->name . 'さん';
}

私の担当しているプロジェクトでは不用意に上記のようなコードを追加してしまっており、N+1 問題だけでなく、このアクセサを使用しない場面でもリレーションが呼ばれて不要なクエリが発行されてしまっていました。
※モデルのインスタンスが作成されるたびにアクセサが実行されます

回避策1:そもそも $appends に書かない

身も蓋もないのですがそのアクセサの情報が必要な箇所でのみ使用するようにしましょう(自戒) 。 $appends には記載しない場合以下のように書けばアクセサをその箇所でのみ追加できます。

 $hoge->append('user_name')

show 系のメソッドで単体のモデルを取得する場合なら、リレーションのクエリも1回しか発行されてないため上記で対応できます。

回避策?という感じではあるのですが、私のプロジェクトでは使用する場面が限られるアクセサも $appends に追加されており、不要な呼び出しが発生していたので記載しました。

回避策2:アクセサが参照しているリレーション先を with で指定する

これは index 系のメソッドでアクセサを定義しているモデルを何十件と取得する場合に有効な回避策です。

冒頭のアクセサが Hoge モデルに書かれているとします。
Hoge モデルを一度に何十件も取ってくる以下のようなクエリに、 アクセサで呼ばれるリレーション先の user を with で指定しておきます。

$hoges = Hoge::query()
    ->with('user')
    ->limit(50)
    ->get();

以下のような流れでアクセサは実行され、N+1 問題は発生しません。

  1. Hoge モデルをまとめて取得するクエリが実行される
  2. Hoge モデルのリレーション先の User モデルをまとめて取得するクエリが実行される
  3. Hoge モデルのアクセサが実行されるが、この時点ではすでにリレーション先の User モデルは取得されているので User モデル取得のクエリは実行されない

回避策3:scope を使う

回避策とは少し違うとは思うのですが、少し複雑なクエリかつ複数箇所で使用する場合に scope を使う方法もあります。

良い例が出せないのですが、以下のように addSelect で行を追加することで擬似的なアクセサのような使い方ができます。
※もちろんこの程度なら使うまでもないのですが…!

public function scopeHogeUserName($query)
    {
        return $query
            ->addSelect(DB::raw("concat(user.name, 'さん') AS user_name"));
    }

まとめ

アクセサを追加する場合は以下の2点を頭に置いておこうと思いました…!

  • できるだけアクセサでクエリを呼ばない
  • クエリを呼ぶ場合は $appends には追加しない