Laravelの強力なORMであるEloquentにおいて、データベース操作を圧倒的に直感的かつ効率的にしてくれる機能が「リレーション(Eloquent Relationships)」です。
しかし、開発を始めたばかりの初心者や中級者にとって、以下のような疑問や混乱が頻繁に発生します。
- 「
hasManyとbelongsToはどっちのモデルに書けばいいの?」 - 「外部キーの名前はどう指定すればいい?Laravelのデフォルト規則は?」
- 「多対多(
belongsToMany)の中間テーブルってどうマイグレーション・操作するの?」 - 「1対1、1対多、多対多、Through、ポリモーフィックなど、リレーションにはどんな種類がある?」
- Laravel ページネーション完全ガイド|UIカスタマイズ・日本語化・検索条件引き継ぎ(withQueryString)まで徹底解説
この記事では、LaravelにおけるEloquentリレーションの全種類一覧、最もつまずきやすい hasMany と belongsTo の違い・見分け方の鉄則、外部キー・主キーの命名規則とカスタマイズ、中間テーブルの操作(sync / attach / detach)、関連データの取得・保存・更新テクニック、N+1問題への対策まで、実務でそのまま使えるコード例とともに徹底解説します。
【早見表】Laravel Eloquentリレーション全種類一覧&使い分け
Laravel Eloquentで利用できる主要なリレーションメソッドの一覧と特徴、外部キーの所在、主なユースケースのまとめです。
| リレーション関係 | メソッド名 | 逆方向メソッド | 外部キーの所在 | 代表的な具体例 |
|---|---|---|---|---|
| 1対1 (One To One) |
hasOne() |
belongsTo() |
相手側(従テーブル) | ユーザー(User) ⇄ プロフィール(Profile) |
| 1対多 (One To Many) |
hasMany() |
belongsTo() |
相手側(「多」側のテーブル) | ユーザー(User) ⇄ 投稿(Post) 投稿(Post) ⇄ コメント(Comment) |
| 多対多 (Many To Many) |
belongsToMany() |
belongsToMany() |
中間テーブル(Pivot Table) | ユーザー(User) ⇄ 役割(Role) 記事(Post) ⇄ タグ(Tag) |
| 1対1(経由) (Has One Through) |
hasOneThrough() |
— | 中間テーブル+相手側 | サプライヤー ⇄ アカウント履歴(ユーザー経由) |
| 1対多(経由) (Has Many Through) |
hasManyThrough() |
— | 中間テーブル+相手側 | 国(Country) ⇄ 投稿(Post)(ユーザー経由) |
| 1対1 ポリモーフィック (One To One Morph) |
morphOne() |
morphTo() |
相手側(*_id, *_type) |
ユーザー / 企業 ⇄ 画像(Image) |
| 1対多 ポリモーフィック (One To Many Morph) |
morphMany() |
morphTo() |
相手側(*_id, *_type) |
投稿 / 動画 ⇄ コメント(Comment) |
| 多対多 ポリモーフィック (Many To Many Morph) |
morphToMany() |
morphedByMany() |
中間テーブル(*_id, *_type) |
投稿 / 動画 ⇄ タグ(Tag) |
一番迷う!hasMany と belongsTo の違いと見分け方
Laravelのリレーションを学ぶ上で、初心者が最初にぶつかる壁が「hasMany と belongsTo のどちらをどちらのモデルに定義するか」です。
この2つの違いと見分け方は、以下の「3つの鉄則」さえ覚えれば二度と迷わなくなります。
鉄則1:外部キー(user_id等)を持つテーブル側に「belongsTo」を書く
データベースのテーブル設計において、外部キー(親テーブルのIDを保持するカラム)を持っているテーブルに対応するモデルには、必ず belongsTo を定義します。
・
users テーブル:id, name, email(外部キーなし → 親側:hasMany / hasOne)・
posts テーブル:id, user_id, title, content(外部キーあり → 子側:belongsTo)
英語の意味通り、「Post(投稿)は User(ユーザー)に所属している(belongs to)」と考えると直感的です。
鉄則2:メソッド名は「hasManyは複数形」「belongsToは単数形」
Laravelの命名規則として、取得できる結果が複数(Collection)になる hasMany は複数形、単一のインスタンス(Model)になる belongsTo は単数形でメソッド名を命名します。
| モデル | リレーション | メソッド名 | 取得されるもの | アクセス例 |
|---|---|---|---|---|
User モデル(親) |
hasMany(Post::class) |
posts()(複数形) |
Illuminate\Database\Eloquent\Collection |
$user->posts |
Post モデル(子) |
belongsTo(User::class) |
user()(単数形) |
App\Models\User(単一モデル) |
$post->user |
hasMany と belongsTo の対比コード
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
// 親モデル:User
class User extends Model
{
/**
* ユーザーが持つ複数の投稿を取得(1対多:hasMany)
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
// 子モデル:Post(外部キー user_id を持つ側)
class Post extends Model
{
/**
* この投稿を所有するユーザーを取得(逆方向:belongsTo)
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Eloquentリレーションの基本:1対1・1対多・多対多の実装例
実務で頻出する3大基本リレーション(1対1、1対多、多対多)のマイグレーション設計とモデル定義を詳しく見ていきましょう。
1. 1対1リレーション(hasOne / belongsTo)
「ユーザー(User)」と「プロフィール(Profile)」や「電話番号(Phone)」のように、1つのレコードに対して相手のレコードが1つだけ存在する関係です。
マイグレーション設計
// database/migrations/xxxx_create_profiles_table.php
Schema::create('profiles', function (Blueprint $table) {
$table->id();
// 外部キー user_id を一意制約(unique)にして1対1を担保
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
$table->string('nickname');
$table->text('bio')->nullable();
$table->timestamps();
});
モデル定義
// App/Models/User.php
use Illuminate\Database\Eloquent\Relations\HasOne;
class User extends Model
{
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
}
// App/Models/Profile.php
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Profile extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
データの利用
// プロフィールの取得
$user = User::find(1);
echo $user->profile->nickname;
// 逆方向からユーザー情報の取得
$profile = Profile::find(1);
echo $profile->user->name;
2. 1対多リレーション(hasMany / belongsTo)
「ユーザー(User)」と「投稿(Post)」、「投稿(Post)」と「コメント(Comment)」など、Webシステムで最も多く使われる関係です。
マイグレーション設計
// database/migrations/xxxx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('content');
$table->boolean('is_published')->default(false);
$table->timestamps();
});
データの利用
$user = User::find(1);
// ユーザーの全投稿をCollectionとして取得
foreach ($user->posts as $post) {
echo $post->title;
}
// 条件を追加してクエリ実行(メソッドとして呼び出し)
$publishedPosts = $user->posts()
->where('is_published', true)
->orderBy('created_at', 'desc')
->get();
3. 多対多リレーション(belongsToMany)と中間テーブル
「ユーザー(User)」と「役割(Role)」、「記事(Post)」と「タグ(Tag)」のように、互いに複数の関連を持つ関係です。多対多では中間テーブル(Pivot Table)を介して関連付けます。
中間テーブルの命名規則
Laravelのデフォルトでは、「関連する2つのモデル名を単数形スネークケースにし、アルファベット順にアンダースコアで結合した名前」を中間テーブル名として認識します。
UserとRole→role_user(rがuよりアルファベット順で前)PostとTag→post_tag(pがtよりアルファベット順で前)- Laravel ページネーション完全ガイド|UIカスタマイズ・日本語化・検索条件引き継ぎ(withQueryString)まで徹底解説
マイグレーション設計
// database/migrations/xxxx_create_post_tag_table.php
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
// 重複登録を防止するユニークインデックス
$table->unique(['post_id', 'tag_id']);
$table->timestamps();
});
モデル定義(両方に belongsToMany を記述)
// App/Models/Post.php
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Post extends Model
{
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)
->withTimestamps(); // 中間テーブルのcreated_at/updated_atを自動管理
}
}
// App/Models/Tag.php
class Tag extends Model
{
public function posts(): BelongsToMany
{
return $this->belongsToMany(Post::class)
->withTimestamps();
}
}
中間テーブルのカスタムカラム(withPivot)
中間テーブルにステータスや権限レベルなどの追加カラムがある場合は、withPivot() を指定します。
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)
->withPivot('assigned_by', 'expires_at')
->withTimestamps();
}
// アクセス方法:pivotプロパティ経由で取得
foreach ($user->roles as $role) {
echo $role->name;
echo $role->pivot->assigned_by; // 中間テーブルのカラム
echo $role->pivot->expires_at;
}
外部キー・主キーの命名規則とカスタマイズ方法
Laravelの命名規約から外れた既存データベースを扱う場合や、カスタムカラム名を使用する場合は、リレーションメソッドの引数で明示的に指定できます。
デフォルトの命名規約
| 項目 | デフォルト規則 | 具体例 |
|---|---|---|
| 外部キー (Foreign Key) | モデル名(単数形スネークケース)_id |
user_id, post_id |
| 親のローカルキー (Local / Primary Key) | id |
users.id, posts.id |
| 中間テーブル名 | モデル単数形アルファベット順 | post_tag, role_user |
カスタムキーの指定方法
// hasMany の場合: 第2引数=外部キー、第3引数=ローカルキー
public function posts(): HasMany
{
return $this->hasMany(
Post::class,
'author_id', // postsテーブル側の外部キーカラム名
'user_code' // usersテーブル側のローカルキーカラム名
);
}
// belongsTo の場合: 第2引数=外部キー、第3引数=親の主キー
public function author(): BelongsTo
{
return $this->belongsTo(
User::class,
'author_id', // postsテーブル側の外部キーカラム名
'user_code' // usersテーブル側の主キーカラム名
);
}
// belongsToMany の場合: 第2引数=中間テーブル名、第3引数=自モデルの外部キー、第4引数=相手モデルの外部キー
public function tags(): BelongsToMany
{
return $this->belongsToMany(
Tag::class,
'custom_post_tags', // カスタム中間テーブル名
'article_id', // post側の外部キー
'label_id' // tag側の外部キー
);
}
リレーションを使ったデータ操作(取得・登録・更新・削除)
Eloquentリレーションの真価は、データの取得だけでなく関連レコードの登録・同期を安全かつシンプルに行える点にあります。
1. 動的プロパティ vs クエリメソッド
| 書き方 | 返り値 | 特徴・使い分け |
|---|---|---|
$user->posts(動的プロパティ) |
Collection |
すでに取得済みの関連レコードにアクセスする(キャッシュされる)。 |
$user->posts()(クエリメソッド) |
Relation / Builder |
さらに where() や orderBy() などのSQLクエリ条件を追加して絞り込む。 |
2. 1対多の登録:save() と create()
リレーション経由で登録すると、親の外部キー(user_id)が自動的にセットされます。
$user = User::find(1);
// create(): 配列を渡して作成(user_idは自動付与)
$post = $user->posts()->create([
'title' => 'Laravelリレーション完全ガイド',
'content' => 'リレーションの定義方法を解説します。',
'is_published' => true,
]);
// save(): すでにインスタンス化したモデルを保存
$newPost = new Post(['title' => '新しい記事']);
$user->posts()->save($newPost);
// saveMany(): 複数モデルを一括保存
$user->posts()->saveMany([
new Post(['title' => '記事1']),
new Post(['title' => '記事2']),
]);
3. 多対多の操作:attach / detach / sync / toggle
多対多の中間テーブル操作には、以下の専用メソッドが用意されています。
$post = Post::find(1);
// 1. attach(): タグIDを追加(重複チェックなしで追加)
$post->tags()->attach(1);
$post->tags()->attach([2, 3]); // 複数一括
$post->tags()->attach(4, ['expires_at' => now()->addDays(7)]); // 中間カラムも同時保存
// 2. detach(): 指定のタグIDを解除(引数なしなら全解除)
$post->tags()->detach(1);
$post->tags()->detach([2, 3]);
$post->tags()->detach(); // post_id=1の全タグ紐付けを削除
// 3. sync(): 画面フォーム更新で最もよく使う!指定したIDのみに同期(不要なものは自動削除)
$post->tags()->sync([1, 3, 5]);
// 4. syncWithoutDetaching(): 既存を残しつつ指定IDを追加同期
$post->tags()->syncWithoutDetaching([6, 7]);
// 5. toggle(): 存在すれば解除、存在しなければ追加
$post->tags()->toggle([1, 2]);
4. belongsTo の更新:associate() と dissociate()
子モデルの親(所属先)を変更する際は、associate() を使うと外部キーを直接書き換えることなく安全に設定できます。
$user = User::find(2);
$post = Post::find(10);
// 所属ユーザーを user_id=2 に変更して保存
$post->user()->associate($user);
$post->save();
// 所属を解除して null にする場合(外部キーが nullable の場合)
$post->user()->dissociate();
$post->save();
応用リレーション:Has-Through と ポリモーフィック
複雑なデータベース設計に対応するための高度なリレーション機能も押さえておきましょう。
1. 経由リレーション(hasOneThrough / hasManyThrough)
中間モデルを経由して遠くのテーブルからデータを取得します。
例:「国(Country)→ ユーザー(User)→ 投稿(Post)」
「ある国に所属する全ユーザーの投稿一覧」を取得したい場合、中間にある User モデルを経由して直接 Post を取得できます。
// App/Models/Country.php
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Country extends Model
{
public function posts(): HasManyThrough
{
// Country -> User (中間) -> Post
return $this->hasManyThrough(Post::class, User::class);
}
}
// 使い方:日本の全ユーザーの投稿を取得
$country = Country::where('name', 'Japan')->first();
$posts = $country->posts;
2. ポリモーフィックリレーション(Polymorphic)
ポリモーフィックリレーションは、「1つのテーブルが複数の異なる親モデルに関連付けられる」機能です。
例えば、「投稿(Post)」と「動画(Video)」の両方に「コメント(Comment)」を付けたい場合、通常は post_comments と video_comments のようにテーブルを分ける必要がありますが、ポリモーフィックを使えば comments テーブル1つで共有できます。
// マイグレーション: comments テーブル
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
// commentable_id と commentable_type (文字列: App\Models\Post など) を作成
$table->morphs('commentable');
$table->timestamps();
});
// App/Models/Comment.php
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
// App/Models/Post.php
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
// App/Models/Video.php
class Video extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
実務で必須!リレーション利用時の注意点&ベストプラクティス
1. N+1問題の発生を防止する(with による Eager Loading)
ループ内でリレーションプロパティにアクセスすると、ループ回数分だけSQLが追加発行される「N+1問題」が発生し、深刻なパフォーマンス低下を招きます。
// ❌ アンチパターン: 1 + N 回のクエリが発行される
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // 投稿ごとに毎回 SELECT * FROM users WHERE id = ... が走る
}
// ⭕ ベストプラクティス: with() で事前一括ロード(2回のクエリで完了)
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // キャッシュ済みのため追加SQLゼロ
}
N+1問題の詳しい仕組みや自動検知設定(Model::preventLazyLoading())については、LaravelのN+1問題を完全解決!with・loadによるEager Loadingと検知・防止テクニック で詳しく解説しています。
2. リレーション先の存在チェック・絞り込み(has / whereHas)
「コメントが1件以上ある投稿のみ取得」「管理者が書いた投稿のみ取得」といったリレーション先の条件による絞り込みには、has() や whereHas() を活用します。
// コメントが1件以上ある投稿を取得
$posts = Post::has('comments')->get();
// コメントが3件以上ある投稿を取得
$popularPosts = Post::has('comments', '>=', 3)->get();
// リレーション先の条件で絞り込み(例: 公開済みのタグを持つ記事)
$posts = Post::whereHas('tags', function ($query) {
$query->where('is_active', true);
})->get();
さらに高度なクエリテクニックは、LaravelのwhereHasメソッドを使った効率的なクエリ構築ガイド をご覧ください。
3. 件数のみ必要な場合は withCount() を使う
リレーション先モデルの全データをロードせず、件数だけを取得したい場合は withCount() を使用します。無駄なメモリ消費を防ぎ、大幅に高速化できます。
// posts_count カラムが自動追加される
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo "{$user->name} さんの投稿数: {$user->posts_count} 件";
}
4. 複数テーブルへの更新はトランザクションで保護する
親レコード作成と子レコード作成(または中間テーブル同期)を同時に行う場合、途中でエラーが起きるとデータ不整合が生じます。必ず DB::transaction() で囲みましょう。
use Illuminate\Support\Facades\DB;
DB::transaction(function () use ($userData, $postData, $tagIds) {
$user = User::create($userData);
$post = $user->posts()->create($postData);
$post->tags()->sync($tagIds);
});
安全なトランザクション実装については、Laravel トランザクションの使い方完全ガイド を参考にしてください。
関連記事・内部リンク
- Laravel Eloquentとは?使い方の基本からCRUD・リレーション・クエリビルダとの違いまで徹底解説
- LaravelのN+1問題を完全解決!with・loadによるEager Loadingと検知・防止テクニック
- Laravel トランザクションの使い方完全ガイド|DB::transactionの自動コミット・手動ロールバック・デッドロック再試行
- LaravelのwhereHasメソッドを使った効率的なクエリ構築ガイド
- Laravel Migrationの使い方完全ガイド|作成・実行・ロールバックとよくあるエラー対処
- Laravel ページネーション完全ガイド|UIカスタマイズ・日本語化・検索条件引き継ぎ(withQueryString)まで徹底解説
まとめ:リレーションを使いこなして美しいEloquent設計を
LaravelのEloquentリレーションを理解することで、SQLの記述量を減らしつつ、保守性が高く堅牢なデータベース操作を実現できます。
- hasMany と belongsTo の違い:外部キーを持つ子テーブル側に
belongsTo、親テーブル側にhasMany - メソッドの命名規則:
hasMany/belongsToManyは複数形、hasOne/belongsToは単数形 - 中間テーブル:アルファベット順・単数形スネークケース(例:
post_tag)。更新にはsync()を活用 - データ登録:
$user->posts()->create([...])で外部キーを自動付与 - パフォーマンス対策:ループ処理時は必ず
with()による Eager Loading を行い、N+1問題を防止 - Laravel ページネーション完全ガイド|UIカスタマイズ・日本語化・検索条件引き継ぎ(withQueryString)まで徹底解説
リレーションのルールと命名規則をマスターし、すっきりと読みやすいLaravelコードを書いていきましょう。

コメント