LaravelのN+1問題を「Eager loading」で解決する

シェアしてね〜🤞

Laravelは、開発者が効率的にアプリケーションを構築できるPHPフレームワークです。しかし、データベースとアプリケーションとの間でのデータのやり取りにおいて、"N+1問題"に遭遇する可能性があります。この記事では、N+1問題とその解決策であるEager Loadingについて、Laravelを使用した具体的なコード例とともに解説します。

N+1問題とは

N+1問題は、データベースとのデータのやり取りにおいて、データを取得する際に遭遇するパフォーマンス上の問題です。

この問題は、1つのクエリで取得したデータを基に、さらにN回のクエリが発行され、結果として合計でN+1回のクエリが実行される状況を指します。

N+1問題の例:記事とコメント

例えば、ブログの記事とそれに対するコメントを取得する状況を考えます。モデルの関係は以下のようになります。

class Post extends Model
{
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

N+1問題が発生するコード例は以下の通りです。

$posts = Post::all();

foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->content;
    }
}

上記のコードは、最初のクエリで全てのポストを取得し(1回)、その後、各記事に対してコメントを取得するクエリを発行します(N回)。

例えば、10個の記事がある場合、

  • 10個の記事を1回のクエリで取得(1回のクエリ)
  • 10個の記事それぞれに対してコメントを取得(10回のクエリ)

合計で11回のクエリが発行されることになります。これがN+1問題の典型的な例です。

この問題は、データベースへのアクセス回数が増えるにつれ、よりアプリケーションのパフォーマンスに悪影響を与える可能性があります。特に、レコード数が多い場合や複雑なクエリを実行する場合、この影響は顕著になります。

次のセクションでは、このN+1問題を解決するための、Laravelの一般的なアプローチであるEager Loadingについて詳しく説明します。

Eager Loading(イーガーローディング)とは

Eager Loadingは、LaravelのEloquent ORMにおいて、リレーションシップのデータを効率的に取得するためのテクニックです。これは、データベースからデータをフェッチする際に、関連するデータも一緒に取得することで、N+1問題を解決します

Eager Loadingの基本的な使用方法

Eager Loadingは、主にwithメソッドを使用して実装されます。このメソッドを使用すると、指定したリレーションシップのデータを初めのクエリで一緒に取得することができます。これにより、後続の操作で追加のクエリを発行する必要がなくなり、データベースへのクエリ数が大幅に削減されます。

以下のコードは、Eager Loadingを使用して、記事とそれに関連するコメントを2回のクエリで取得する例です。

$posts = Post::with('comments')->get();

foreach ($posts as $post) {
    foreach ($post->comments as $comment) {
        echo $comment->content;
    }
}

このコードでは、with('comments')を使用することによって、2つのクエリを発行します。最初のクエリは、すべての記事を取得するためのものです。次に、取得したすべての記事に紐づくCommentを一度のクエリで取得します。

この方法で記事の数に関係なく、紐づくCommentを取得するためには常に1つのクエリしか発行されません。これにより、N+1問題が解消されます。

$post->comments と $post->comments() の違い

Laravelのリレーションシップを扱う際、$post->comments と $post->comments() は似ているように見えますが、実際には大きく異なります。

  • $post->comments
    • Eager Loadingによって事前に取得したリレーションデータを参照します。この場合、追加のデータベースクエリは発行されません。
  • $post->comments()
    • このメソッドは新しいクエリをデータベースに発行してリレーションデータを取得します。Eager Loadingの利点を活かすためには、このメソッドの使用は避けるべきです。

この2つの違いは、()の有無だけですが、その影響は大きいです。特に、パフォーマンスを最適化するためにEager Loadingを使用する際には、この違いを正確に理解し、適切に使用することが重要です。

詳細はこちらのLaravelの公式ドキュメントを参照してください。

応用例:Eager Loadingの利用シーン

ここまではEager Loadingの基本的な使い方を見てきましたが、このセクションではその応用的な使い方を実例を通して見ていきましょう。

リレーションデータの条件付き取得

Eager Loadingを使用する際、特定の条件に基づいてリレーションデータを取得することも可能です。これは、関連データが多く、その中から特定のデータのみをフェッチしたい場合などに非常に便利です。

$posts = Post::with(['comments' => function ($query) {
    $query->where('is_active', true);
}])->get();

上記のコードでは、記事ごとのコメントを取得しますが、commentテーブルのis_active カラムが true であるデータのみを取得します。この方法で、不要なデータを省き、必要なデータのみを効率的に取得できます。

ネストされたリレーションデータの取得

複数のリレーションを持つデータを扱う場合、ネストされたリレーションデータを一度のクエリで取得することもできます。以下のようなデータの関係性を例に説明します。

  • Post: ブログの記事を表すモデルです。
  • Comment: ブログの記事に対するコメントを表すモデルです。各Commentは特定のPostに紐づいています。
  • Author: コメントの著者を表すモデルです。ここでは、コメントを書いたユーザーを指します。各Commentは特定のAuthorに紐づいています。
$posts = Post::with('comments.author')->get();

このコードは、各記事と、記事に紐づくコメント、さらに各コメントの著者を一度のクエリで取得します。これにより、後続の操作で各リレーションを個別にクエリする必要がなくなり、パフォーマンスが向上します。

既に取得された親データに対するリレーションデータの追加取得

すでに取得された記事のデータに対して、後からリレーションデータを取得することも可能です。これは、特定の条件下でのみ追加のリレーションデータが必要な場合などに役立ちます。

$posts = Post::all();

$posts->load('comments');

ここでは、全ての記事を取得した後、それぞれの記事に紐づくコメントを追加で取得しています。load メソッドを使用することで、既存のモデルインスタンスに対して追加のリレーションデータを後から取得できます。

この例では以下のようなクエリを発行します。

SELECT * FROM posts;
SELECT * FROM comments WHERE post_id IN (1, 2, 3, ...);

ここでの(1, 2, 3, ...)は取得された記事のIDのリストを示しています。

合計で、データベースへのクエリは2回発行されます。

このようにすでに取得されている記事データに対して$posts->load('comments')を使うことで、複数の記事に紐づくコメントを1回のクエリで取得することができます。

これらの応用例を通して、Eager Loadingはアプリケーションのパフォーマンスを向上させ、DB操作を最適化する強力なツールであることがわかります。

適切に利用することで、データベースへのアクセスの効率を向上させられる可能性があります。

Eager Loadingの注意点とその適切な使用法

リレーションメソッドの使用とその影響

LaravelのEager Loadingは、データベースとのデータのやりとりを効率化するための強力な機能です。しかし、その使用方法によっては、その利点が損なわれることがあります。特に、リレーションメソッドの使用方法には注意が必要です。

リレーションメソッドの誤った使用例

$posts = Post::with('comments')->get();

foreach ($posts as $post) {
    $comments = $post->comments()->where('approved', true)->get();
    foreach ($comments as $comment) {
        echo $comment->content;
    }
}

上記のコードでは、with('comments')を使用してcommentsリレーションをEager Loadingしているにも関わらず、foreach文の中でcomments()メソッドを使用して新しいクエリを発行しています。このcomments()メソッドは、新しいクエリをデータベースに発行しているため、Eager Loadingの本来の目的である、データベースへのクエリ数を減らすことを無効にしてしまいます。

正しいリレーションデータのアクセス方法

$comment = $post->comments;

こちらのコードは、Eager Loadingによって事前に取得されたcommentsリレーションデータにアクセスしています。commentsプロパティは、動的プロパティとしてリレーションデータを取得します。これにより、新しいクエリが発行されず、キャッシュされたリレーションデータが使用されるため、パフォーマンスが向上します。

N+1問題は、ORMを使用したデータベースの操作において、パフォーマンスのボトルネックとなる可能性があります。Eager Loadingは、Laravelアプリケーションにおけるデータベース操作でクエリを投げる回数を減らせる可能性があります。

しかし、間違った使い方ではその利点が損なわれることがあるため、適切な使用と注意点を理解し、アプリケーションの各部分で効果的に利用することで、パフォーマンスの向上を目指しましょう。

シェアしてね〜🤞