この記事でわかること
- N+1問題は「気づかないうちに」パフォーマンスとUXを破壊する
- 開発環境で
Model::preventLazyLoading()を有効化し、N+1をシステム的に検知・ブロックすべき - リレーション先のデータが必要な場合は、必ず
with()(Eager Loading)を使う
はじめに
Laravelを使った開発で、誰もが一度は直面する(あるいは気づかずに放置してしまう)のが 「N+1問題」 です。
ローカル環境ではデータ量が少ないためサクサク動いていても、本番環境にデプロイしてデータが増えた途端に「画面の表示が異常に遅い…」という事態を引き起こします。
画面の表示速度の低下は、ユーザーの離脱率を高め、結果的にビジネス上の機会損失(UXの悪化)に直結します。
この記事では、Laravel 12での実装において「N+1問題を未然に防ぐための仕組み作り」と、その解決策についてまとめました。
1. 気づかぬうちに潜む「N+1問題」とは?
N+1問題とは、データベースからデータを取得する際、「親モデルを取得する1回のクエリ」+「子モデルを取得するN回のクエリ」 が発行されてしまうパフォーマンスのボトルネックです。
例えば、「ブログ記事(Post)」とその「作成者(User)」を表示する場合を考えます。
// ❌ 悪い例(Lazy LoadingによるN+1発生)
$posts = Post::all(); // ① 記事を全件取得するクエリ(1回)
foreach ($posts as $post) {
// ② ループのたびに、その記事の作成者を取得するクエリが走る(N回)
echo $post->user->name;
}
もし記事が100件あれば、合計101回のSQLが発行されてしまいます。これがN+1問題です。
2. 仕組みで未然に防ぐ:preventLazyLoading() の重要性
N+1問題の厄介なところは、コードとしてはエラーにならず、正しく動いてしまうことです。そのため、コードレビューや手動のテストで見逃されがちです。
そこで絶対に活用したいのが、Laravelに備わっている 遅延読み込み(Lazy Loading)の無効化機能 です。
設定方法
App\Providers\AppServiceProvider の boot メソッドに以下を追記します。
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
// 本番環境以外(ローカルやテスト環境)でのみ、遅延読み込みを禁止する
Model::preventLazyLoading(! app()->isProduction());
}
なぜこれが重要なのか?
この1行を追加するだけで、開発中にN+1問題を引き起こすコード(前述の悪い例など)が実行された瞬間、即座に例外(Exception)を投げて画面をエラーにしてくれます。
「後で気づいて直す」のではなく、「開発環境の時点で絶対にN+1を許さない仕組み」を作ることが、堅牢でUXを損なわないアプリケーション作りの鉄則です。
3. 具体的な解決策:with() による Eager Loading
preventLazyLoading() でN+1を検知できたら、正しいクエリに修正します。
解決策はシンプルで、with() メソッドを使って Eager Loading(事前割り当て) を行うだけです。
// ⭕️ 良い例(Eager Loading)
// 記事を取得する際に、関連するユーザー情報も一緒に取得しておく
$posts = Post::with('user')->get();
foreach ($posts as $post) {
// すでに取得済みなので、ここではSQLは発行されない!
echo $post->user->name;
}
これにより、発行されるSQLは「記事を取得するクエリ(1回)」と「そこに関連するユーザーをIN句でまとめて取得するクエリ(1回)」の合計2回に劇的に削減されます。
まとめ
システムをレガシーからモダンへ移行していく中で、「動けばいい」から一歩進んで「パフォーマンスとUXを担保できる設計」を意識することは非常に重要です。
- 開発環境では
Model::preventLazyLoading(!app()->isProduction());を必ず設定し、検知を自動化する。 - リレーションを取得する際は
with()を使ってEager Loadingを徹底する。
これらのベストプラクティスを仕組みとして取り入れ、今後の実装でも積極的に活用していきたいと思います!