Laravel 8.43.0 の Preventing Lazy Loading 機能で、N+1問題を早めに発見してみる

2 min read読了の目安(約2000字

はじめに

Laravel 8.43.0 の目玉機能の「Preventing Lazy Loading」を試してみました。

これは何かと言うと、Eloquent Model で Lazy Loading している場合、エラーにしてしまうという機能です。そうすることで、DBの N+1 問題を起こしている箇所を早めに見つけようという話です。

本家ドキュメント:Preventing Lazy Loading

早速試す

まず、AppServiceProvider の boot() などで、以下のように記述して、この機能を有効にします。

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

引数は、この機能を有効にする為の条件で、上記の場合、productionサーバだったら無効にするという事です。

以降では、Post belongsTo User と想定し、Postモデルには以下のメソッドがあるとします。

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

後はサクッと試してみます。以下のように記述します。

    $posts = Post::get();

    foreach ($posts as $post) {
        $post->user->name; // lazy load 発生!
    }

これで、

Illuminate\Database\LazyLoadingViolationException
Attempted to lazy load [user] on model [App\Models\Post] but lazy loading is disabled.

という感じのエラーが表示されて処理がストップします。
もしこの機能をオンにしていなければ、N+1問題が発生していた所です。

では、下記のように書き換えると、

    $posts = Post::with('user')->get(); // Eagerロード

    foreach ($posts as $post) {
        $post->user->name;
    }

問題無く処理が実行されます。N+1問題も発生しません。

で、注意点や制限事項は?

lazy load を指摘してくれますが、動的プロパティを介さず、リレーションメソッドを直接呼び出している場合には、指摘してくれません。

例えば、

    $posts = Post::get();

    foreach ($posts as $post) {
        $post->user()->first()->name;
    }

結局これも lazy load なのですが、こちらの場合はエラーになりません。
ですが、N+1問題は、発生しています。

(追記)以下は、8.44.0 で修正されましたので、エラーとならなくなります。

もう1つ。
複数のモデルを取得している訳では無く、1つだけだとしても、動的プロパティで lazy load していれば、エラーとなります。下記のような場合です。

    $post = Post::first();

    $post->user->name;

この場合、N+1問題は関係ないですが、エラーとなっていまいます。
回避したければ、素直に ::with('user') するか、又はメソッド形式で呼び出すしかないですね。

技術的には下記も可能ですが、まぁ普通はしないでしょう。~

    $post = Post::first();
    $post->preventsLazyLoading = false;

    $post->user->name;

雑感

便利と言えば便利ですね。
まぁ、たまには?Lazy Loading は、Lazy Loading なりの使い所もあるでしょうから、その辺は要調整といった感じでしょうか。

おかしな箇所等ありましたら、コメント下さい。