Laravel with — Eloquentのリレーションをまとめて取得するメソッド

実装・応用テクニック

Laravelのwith()は、Eloquentモデルを取得する際にリレーション先のデータもまとめて読み込む「イーガーローディング(eager loading)」のためのメソッドです。with()を使わずにリレーションへアクセスすると、ループのたびに追加クエリが発行される「N+1問題」が発生しやすくなります。この記事では、with()の基本的な使い方から、ネストしたリレーションの取得、条件付きイーガーローディング、withCount()load()との使い分け、join()との違いまで、実際に発行されるSQLを確認しながら解説します。

Laravelのwith()メソッドとは

with()は、Eloquentのクエリビルダに対してリレーション名を渡すことで、そのリレーションを1回の追加クエリでまとめて取得できるようにするメソッドです。デフォルトの遅延ローディング(lazy loading)と対になる仕組みで、モデルを複数件扱う処理では基本的にwith()を使うことが推奨されます。リレーションの定義方法自体はLaravel Eloquentとは?使い方の基本からリレーション・クエリビルダとの違いまで徹底解説で扱っているため、ここではwith()そのものの挙動に絞って解説します。

検証にはAuthor(著者)が複数のBook(書籍)を持つhasManyのリレーションを使います。

class Author extends Model
{
    public function books(): HasMany
    {
        return $this->hasMany(Book::class);
    }
}

with()を使わない場合に起きるN+1問題

まずwith()を使わずに著者一覧とその書籍を取得すると、著者の件数に比例してクエリが増えることを実際のクエリログで確認します。

$authors = Author::all();

foreach ($authors as $author) {
    $author->books; // アクセスするたびに都度クエリが発行される
}

著者が2件の場合、実際に発行されるSQLは次の3本になります。

select * from "authors"
select * from "books" where "books"."author_id" = ? and "books"."author_id" is not null
select * from "books" where "books"."author_id" = ? and "books"."author_id" is not null

著者が100件あれば書籍取得だけで100回のクエリが発行される計算になり、これが「N+1問題」と呼ばれる典型的なパフォーマンス劣化パターンです。

with()で1対多リレーションをまとめて取得する

同じ処理をwith('books')で書き換えると、クエリが2本に減ります。

$authors = Author::with('books')->get();

foreach ($authors as $author) {
    $author->books; // 追加クエリは発行されない
}
select * from "authors"
select * from "books" where "books"."author_id" in (1, 2)

著者の件数がいくつ増えても、書籍取得のクエリはwhereInによる1本のままです。著者数が多いページほどwith()の効果は大きくなります。

複数・ネストしたリレーションをまとめて取得する

複数のリレーションを同時に取得したい場合は配列で指定します。ネストしたリレーション(Book > Review)はドット記法でつなげます。

// Bookのauthorとreviewsを同時にイーガーローディング
$books = Book::with(['author', 'reviews'])->get();

// Authorからbooks、さらにそのreviewsまで一括取得(ドット記法)
$authors = Author::with('books.reviews')->get();
select * from "authors"
select * from "books" where "books"."author_id" in (1, 2)
select * from "reviews" where "reviews"."book_id" in (1, 2, 3)

books.reviewsと指定した場合でも、階層ごとに1本ずつ、合計3本のクエリで済みます。階層がさらに深くなっても発行されるクエリ本数は「階層の数」に収まり、対象件数には比例しません。

クロージャで条件を絞り込む(条件付きイーガーローディング)

with()には配列のキーにクロージャを渡すことで、取得するリレーション側に絞り込み条件を追加できます。

$authors = Author::with(['books' => function ($query) {
    $query->where('published_year', '>=', 1910);
}])->get();
select * from "authors"
select * from "books" where "books"."author_id" in (1, 2) and "published_year" >= ?

「著者ごとに1910年以降の書籍だけをまとめて取得する」といった条件を、N+1を発生させずに実現できます。

取得するカラムを絞り込む

リレーション先のテーブルにカラム数が多い場合、'リレーション名:カラム名,カラム名'の記法で必要なカラムだけに絞れます。外部キー(この例ではauthor_id)は関連付けに必要なため必ず含めます。

$authors = Author::with(['books:id,author_id,title'])->get();
select * from "authors"
select "id", "author_id", "title" from "books" where "books"."author_id" in (1, 2)

件数だけ欲しいときはwithCount()

関連レコードの中身ではなく件数だけが必要な場合は、with()ではなくwithCount()を使うと、サブクエリ1本で件数まで取得できます。

$authors = Author::withCount('books')->get();

foreach ($authors as $author) {
    echo "{$author->name}: {$author->books_count} books\n";
}
select "authors".*, (select count(*) from "books" where "authors"."id" = "books"."author_id") as "books_count" from "authors"

取得済みのモデルに後からリレーションを読み込む:load()

すでに取得済みのモデルやコレクションに対して、後からイーガーローディングしたい場合はload()を使います。条件分岐で必要なときだけリレーションを読み込みたい場合に便利です。

$authors = Author::all();

// このタイミングでbooksを一括読み込み(遅延イーガーローディング)
$authors->load('books');

with()とjoin()の違い

with()join()はどちらも関連データを扱いますが、目的と返ってくるデータの形が異なります。

  • with():親モデルと子モデルをそれぞれ別クエリで取得し、Eloquentのリレーションとしてネストしたオブジェクト(コレクション)の形で結果を組み立てる。親1件に対して子が複数あっても行が重複しない。
  • join():SQLレベルでテーブルを結合し、1つのフラットな結果セットとして返す。親に対して子が複数あると行が重複するため、集計や横断的な絞り込みに向いている。

「関連データをオブジェクトとして扱いたい」場合はwith()、「複数テーブルを横断した集計・絞り込みをSQL側で行いたい」場合はjoin()という使い分けが基本です。join()の実装例はEloquentでの複数テーブルのデータ統合:Laravel Joinの実践ガイドを参照してください。

with()を使う際の注意点

  • 必要なリレーションだけを指定する:使わないリレーションまでwith()に含めると、無駄なクエリとメモリ消費が増えます。
  • ループ内でwith()を書かないwith()はループに入る前、コレクション取得時点で1回だけ指定します。ループの中で都度呼び出すとイーガーローディングの意味がなくなります。
  • ソフトデリートしたリレーション先も含めたい場合with()単体では論理削除済みのレコードは取得されません。含める場合はwithTrashed()と組み合わせる必要があります。
  • 本当にN+1が解消されているか確認するLaravel Debugbarなどで実際のクエリ本数を確認する習慣をつけると、見落としを防げます。

よくある質問

with()を2回チェーンしたらどうなりますか?

Model::with('books')->with('reviews')のようにチェーンした場合、両方のリレーションがまとめてイーガーローディング対象になります。配列でwith(['books', 'reviews'])と書くのと同じ結果になるため、可読性を優先してどちらの書き方でも問題ありません。

with()とjoin()はどちらを使うべきですか?

Eloquentのモデルとして関連データを扱いたい通常のCRUD処理ではwith()を使います。複数テーブルを横断した集計や、結合結果を直接絞り込みたい場合はjoin()を検討してください。

N+1問題が発生していないか、どうやって確認できますか?

この記事のようにDB::enableQueryLog()で発行されたSQLを確認する方法のほか、開発環境ではLaravel Debugbarを使うとリクエストごとのクエリ本数を画面上で確認できます。

ソフトデリートされたデータもwith()で取得できますか?

通常のwith()では論理削除済みのレコードは対象外になります。削除済みデータも含めて取得したい場合はwithTrashed()との組み合わせが必要です。

まとめ

with()はEloquentでN+1問題を避けるための基本かつ最重要のメソッドです。単一リレーションの取得だけでなく、ネストしたリレーション、クロージャによる条件絞り込み、カラム指定、withCount()load()との使い分けまで押さえておくと、実務でのクエリ最適化に直結します。まずは自分のコードで実際に発行されているSQLを確認し、不要なクエリが発生していないかをチェックすることから始めてみてください。

レン (Wren)

こんにちは。レンです。

Laravelのコードの森に住んでいる、小さな案内役です。
ルーティングの枝やクラスの影を歩きながら、コードの流れや仕組みを眺めています。

このサイトでは、Laravelの基本から実装のコツまで、開発で役立つポイントを静かに整理しています。
難しいことを増やすのではなく、コードの見通しが少し良くなるヒントを届けるのが役目です。

「この処理はどこに書くのがいいのか」
「Laravelではどう考えると整理できるのか」

そんな疑問に、小さなメモを残すような気持ちで記事を書いています。

コードを書いている途中で迷ったとき、
このサイトが少し立ち止まって整理できる場所になればうれしいです。

レン (Wren)をフォローする

コメント