【Laravel】Eloquentのリレーション完全ガイド|hasOne・hasMany・belongsTo・belongsToMany
Laravel の Eloquent ORM(オブジェクトとテーブルを対応づける仕組み)では、テーブル間の関係を「リレーション」として PHP のメソッドで表現します。SQL の JOIN(複数テーブルを結合して取得するクエリ)を直接書かなくても、モデルのメソッドを通じてリレーション先のデータを取得できるため、コードの見通しがよくなります。本記事では hasOne・hasMany・belongsTo・belongsToMany の 4 種類を中心に、定義方法・取得方法・よくある失敗を整理します。
動作の細部はバージョンによって異なる場合があります。コード例は執筆時点で Laravel 10 を主な参照先とします。手元の composer.json の laravel/framework に合わせ、公式ドキュメントの Eloquent: Relationships 章 を並行して参照することをおすすめします。
はじめに
「ユーザーとプロフィールを一緒に取得したい」「投稿に紐づくコメントを一覧で表示したい」という要件は、Laravel アプリケーションで日常的に発生します。テーブルをまたいだデータ取得を毎回手書きの SQL で行うと、コードが冗長になりがちです。
Eloquent のリレーションを使うと、「User モデルは Profile を 1 つ持つ」「Post モデルは複数の Comment を持つ」といった関係をモデルのメソッドとして定義できます。定義後は $user->profile や $post->comments のような直感的な記述でアクセスできます。
この記事では次の範囲を扱います。
- 4 種のリレーション(hasOne / hasMany / belongsTo / belongsToMany)の定義と使い方
- Eager Loading による N+1 問題の回避
- belongsToMany で使う中間テーブル(ピボットテーブル)の操作
- よくある失敗例と対処
背景:4 種のリレーションの概要
| メソッド | 意味 | 例 |
|---|---|---|
hasOne | 1 対 1(親側) | User → Profile |
hasMany | 1 対 多(親側) | User → Post |
belongsTo | 1 対 1 / 1 対多(子側) | Post → User |
belongsToMany | 多対多 | Post ↔ Tag |
外部キーの命名規則は、デフォルトで「呼び出し元モデルのスネークケース名(単語をアンダースコアでつなぐ形式)+ _id」です。たとえば User モデルの hasMany(Post::class) は、posts テーブルの user_id カラムを外部キーとして使います。命名が異なる場合は第 2 引数で明示します。
実装例
hasOne:1 対 1 リレーション
hasOne は「1 つのモデルが 1 つの関連モデルを持つ」関係を表します。User と Profile の例で説明します。
profiles テーブルには user_id カラムが必要です。
// app/Models/User.php
use Illuminate\\Database\\Eloquent\\Relations\\HasOne;
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
取得は次のように行います。
$user = User::find(1);
$profile = $user->profile; // Profile モデルまたは null
プロフィールが存在しない場合に空のモデルを返したいときは withDefault を使います。存在しないときの null チェックを減らせます。
public function profile(): HasOne
{
return $this->hasOne(Profile::class)->withDefault();
}
withDefault はデフォルト値を配列で指定できます。詳細は 公式ドキュメントの Default Models を参照してください。
hasMany:1 対多リレーション
hasMany は「1 つのモデルが複数の関連モデルを持つ」関係を表します。User と Post の例を示します。
posts テーブルには user_id カラムが必要です。
// app/Models/User.php
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
取得すると Laravel の Collection(配列に似たオブジェクト)が返ります。
$user = User::find(1);
$posts = $user->posts; // Post モデルのコレクション
絞り込みはリレーションにメソッドチェーンで追加できます。$user->posts() でクエリビルダに切り替えてから where などの条件を追加します。
$publishedPosts = $user->posts()->where('status', 'published')->get();
belongsTo:逆リレーション(子側の定義)
belongsTo は hasOne / hasMany の逆側(外部キーを持つモデル)で定義します。Post → User の例です。
// app/Models/Post.php
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
外部キーのカラム名が規則と異なる場合(例: author_id)は第 2 引数で指定します。
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
取得は次のように行います。
$post = Post::find(1);
$user = $post->user; // User モデルまたは null
belongsToMany:多対多リレーション
belongsToMany は「互いに複数の関連を持つ」関係を表します。Post と Tag の例では、1 つの Post は複数の Tag を持ち、1 つの Tag も複数の Post に属せます。
中間テーブル(ピボットテーブル)が必要です。テーブル名はデフォルトで 2 モデル名をアルファベット順にアンダースコアでつないだ post_tag になります。中間テーブルのマイグレーションは database/migrations/ に追加します。
// app/Models/Post.php
use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
// app/Models/Tag.php
public function posts(): BelongsToMany
{
return $this->belongsToMany(Post::class);
}
中間テーブルのマイグレーション例です。
Schema::create('post_tag', function (Blueprint $table) {
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['post_id', 'tag_id']);
});
ピボットテーブルの操作
関連の追加・削除・同期は専用メソッドで行います。
$post = Post::find(1);
// タグを追加(重複しない)
$post->tags()->attach([1, 2, 3]);
// タグを削除
$post->tags()->detach([2]);
// 指定した ID だけが紐づいた状態にする(他は削除)
$post->tags()->sync([1, 3]);
ピボットテーブルに追加カラム(例: sort_order)がある場合は withPivot で指定します。
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->withPivot('sort_order');
}
// アクセス方法
foreach ($post->tags as $tag) {
echo $tag->pivot->sort_order;
}
タイムスタンプを記録したい場合は withTimestamps を追加します。
return $this->belongsToMany(Tag::class)->withTimestamps();
Eager Loading で N+1 問題を回避する
リレーションを使う際に最も注意すべきなのが N+1 問題です。N+1 問題とは、N 件のデータをループで処理するときにリレーション先を都度取得し、最終的に N+1 回の SQL が発行されてしまうパターンです。次のコードはループのたびに SQL が実行されます。
// N+1 問題の例(posts の件数分 SQL が発行される)
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // ループごとに SELECT が走る
}
with を使った Eager Loading で、関連データを最初にまとめて取得できます。
// Eager Loading(SELECT は 2 回だけ)
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name;
}
複数のリレーションを同時にロードできます。
$posts = Post::with(['user', 'tags'])->get();
すでに取得済みのコレクションにリレーションを後から読み込む場合は load を使います。
$posts = Post::all();
$posts->load('user');
常に Eager Loading したいリレーションはモデルの $with プロパティで指定します。
// app/Models/Post.php
protected $with = ['user'];
実装時の注意点
外部キーの命名規則を確認する
Eloquent は呼び出し元モデル名をスネークケースにして _id を付加した名前を外部キーとして推測します。User モデルなら user_id です。テーブル設計が規則と異なる場合は、リレーションメソッドの引数で明示的に指定してください。
belongsTo の定義忘れで null が返る
hasMany 側だけを定義して belongsTo 側を定義し忘れると、逆方向のアクセス($post->user など)で null が返ります。双方向アクセスが必要な場合は両モデルに定義が必要です。
ピボットテーブルのテーブル名と外部キー名
belongsToMany のピボットテーブル名とキー名が慣例と異なる場合は、第 2・第 3・第 4 引数で明示します。
return $this->belongsToMany(
Tag::class,
'article_tags', // ピボットテーブル名
'article_id', // 自身の外部キー
'label_id', // 相手の外部キー
);
失敗例:Eager Loading を忘れて本番で大量の SQL が発行される
開発中は件数が少ないためリレーションアクセスのたびに SQL が走っても気づきにくいです。本番環境で件数が増えると急激にパフォーマンスが低下します。
// 1000 件の posts があれば SQL が 1001 回発行される
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
}
// with で 2 回に抑えられる
$posts = Post::with('user')->get();
Laravel Debugbar(リクエスト内のクエリ一覧を確認できる開発用ツール)などを導入して SQL 発行数を可視化すると、N+1 問題を早期に発見しやすくなります。
学び:リレーションで変わること
Eloquent のリレーションを正しく定義すると、コードベースが次のように整理されやすくなります。
データ取得ロジックがモデルにまとまる
コントローラや Service から JOIN を毎回書かずに、$user->posts や $post->tags のような読みやすい表現でアクセスできます。
Eager Loading で SQL 発行を制御できる
with を使うことでクエリ数を明示的にコントロールでき、N+1 問題を構造的に防ぎやすくなります。
双方向アクセスが可能になる
hasMany と belongsTo をセットで定義すると、双方向のアクセスが可能な設計になります。User 側からは Posts、Post 側からは User を参照できます。
まとめ
hasOne/hasManyは「外部キーを相手テーブルに持つ」関係を親側から定義する。belongsToは外部キーを持つモデル(子側)から親側へのアクセスを定義する。belongsToManyには中間テーブルが必要で、attach/detach/syncで操作する。- N+1 問題は
withによる Eager Loading で回避する。
次に試せること
User→Post→Commentのように 3 テーブルをまたいでwith('posts.comments')でネストした Eager Loading を試す。whereHasを使ってリレーション先の条件でフィルタリングする(公式ドキュメントの Querying Relationship Existence)。hasManyThrough/hasOneThroughによる中間テーブル越しのリレーションを把握する。



