Eloquent リレーション図解
テーブルのつながりを、図で一気につかむ
Eloquentのリレーションは「どのテーブルが外部キーを持ち、どちらを指すか」が分かると一気にラクになります。 ここでは各リレーションを関係の図(箱と矢印)・モデル定義コード・使い方コード・具体例の4点セットで解説します。最後に、必ずつまずくN+1問題と Eager Loadingもまとめました。
hasOne / belongsTo(1対1)
1つのモデルが、別の1つのモデルとちょうど1対1で結びつく関係。「ユーザーは1つのプロフィールを持つ」など。
いつ使うか
1件に対して関連データがちょうど1件だけのとき。テーブルを分けて主テーブルを軽くしたい・任意情報を切り出したいときに使います(例: users と profiles)。
外部キーの向き
「持たれる側(子)」が外部キーを持ちます。profiles テーブルに user_id を置き、profiles → users を指します。親(User)側が hasOne、子(Profile)側が belongsTo です。
関係の図
users
User
- id
- name
profiles
Profile
- id
- user_id→ users.id
- bio
- profiles.user_idusers.id1 — 1— 外部キー user_id は子(profiles)側にあり、親(users)を指す
モデル定義コード
親: User モデル(hasOne)
class User extends Model
{
// ユーザーは1つのプロフィールを持つ
public function profile()
{
return $this->hasOne(Profile::class);
// 外部キーを明示するなら:
// return $this->hasOne(Profile::class, 'user_id', 'id');
}
}子: Profile モデル(belongsTo = 逆向き)
class Profile extends Model
{
// プロフィールは1人のユーザーに属する
public function user()
{
return $this->belongsTo(User::class);
}
}使い方コード
親から子をたどる
$user = User::find(1);
$bio = $user->profile->bio; // hasOne 経由でプロフィール取得子から親をたどる(逆)
$profile = Profile::find(1);
$name = $profile->user->name; // belongsTo 経由でユーザー取得具体例
会員サイトで、ログイン情報を持つ users テーブルと、自己紹介や住所などを持つ profiles テーブルを分けるケース。1ユーザーにプロフィールは1つだけなので hasOne / belongsTo が最適です。
hasMany / belongsTo(1対多)
1つの親が複数の子を持つ、最もよく使う関係。「ユーザーは複数の投稿を持つ」「投稿は1人のユーザーに属する」。
いつ使うか
1件に対して関連データが何件もぶら下がるとき。ブログ記事・コメント・注文明細など、実務で圧倒的に登場する定番です。
外部キーの向き
「多」側(子)が外部キーを持ちます。posts テーブルに user_id を置き、posts → users を指します。親(User)が hasMany、子(Post)が belongsTo です。
関係の図
users
User
- id
- name
posts
Post
- id
- user_id→ users.id
- title
- posts.user_idusers.id1 — 多— 外部キー user_id は「多」側(posts)にあり、親(users)を指す
モデル定義コード
親: User モデル(hasMany)
class User extends Model
{
// ユーザーは複数の投稿を持つ
public function posts()
{
return $this->hasMany(Post::class);
}
}子: Post モデル(belongsTo = 逆向き)
class Post extends Model
{
// 投稿は1人のユーザーに属する
public function user()
{
return $this->belongsTo(User::class);
}
}使い方コード
親から子の一覧をたどる
$user = User::find(1);
foreach ($user->posts as $post) { // Collection(複数)が返る
echo $post->title;
}
$count = $user->posts()->count(); // メソッド呼び出しで件数だけ取得子から親をたどる / 子を作成
$post = Post::find(10);
echo $post->user->name; // 投稿者の名前
// 親経由で子を作成すると user_id が自動でセットされる
$user->posts()->create(['title' => '初投稿']);具体例
ブログで「1人のユーザーが何本も記事を書く」関係。User::find(1)->posts でその人の全記事、Post::find(10)->user で記事の著者をたどれます。外部キー user_id は記事(多い側)に置きます。
belongsToMany(多対多・中間テーブル / pivot)
両側が複数を持ち合う関係。「記事は複数のタグを持ち、タグも複数の記事に付く」。間に中間テーブル(pivot)を挟みます。
いつ使うか
どちらから見ても相手が複数になるとき。記事とタグ、ユーザーと役割(ロール)、生徒と講座など。組み合わせを記録する専用テーブルが必要です。
外部キーの向き
外部キーは両側のどちらでもなく、中間テーブル(post_tag)が両方への外部キー(post_id と tag_id)を持ちます。中間テーブル名は原則、関連する2つの単数形をアルファベット順に _ でつなぎます(post + tag → post_tag)。
関係の図
posts
Post
- id
- title
post_tag
(中間テーブル)
- post_id→ posts.id
- tag_id→ tags.id
tags
Tag
- id
- name
- post_tag.post_idposts.id多 — 多— 中間テーブルが post_id で posts を指す
- post_tag.tag_idtags.id多 — 多— 中間テーブルが tag_id で tags を指す
モデル定義コード
両側に belongsToMany を書く
class Post extends Model
{
public function tags()
{
// 中間テーブル post_tag を介して多対多
return $this->belongsToMany(Tag::class);
}
}
class Tag extends Model
{
public function posts()
{
return $this->belongsToMany(Post::class);
}
}使い方コード
たどる / 付け外し
$post = Post::find(1);
foreach ($post->tags as $tag) { // 紐づく全タグ
echo $tag->name;
}
$post->tags()->attach($tagId); // 関連を1件追加(中間に行を作る)
$post->tags()->detach($tagId); // 関連を1件削除
$post->tags()->sync([1, 2, 3]); // この3つだけに一括で揃える中間テーブルの追加情報(pivot)
// マイグレーションで中間テーブルに列を足しておくと…
return $this->belongsToMany(Tag::class)->withPivot('created_by')->withTimestamps();
foreach ($post->tags as $tag) {
echo $tag->pivot->created_by; // 中間テーブルの値に $tag->pivot でアクセス
}具体例
記事にタグを付ける機能。1記事に複数タグ、1タグは複数記事に付くので belongsToMany。attach/detach/sync で紐付けを操作します。「誰が付けたか」など関係そのものの情報は中間テーブルに列を足し、pivot でアクセスします。
hasManyThrough / hasOneThrough(中間モデル経由)
間に別のモデルを1つ挟んで、離れたモデルへ一気にたどる関係。「国 → ユーザー → 投稿」を、国から投稿へ直接。
いつ使うか
A→B→C と2段でつながっているとき、中間のBを毎回書かずにAからCへ直接アクセスしたい場合。多対多とは違い、間にあるのは中間「モデル(テーブル)」で、pivotではありません。
外部キーの向き
外部キーは通常の1対多と同じく、下流のテーブルが上流を指します。users が country_id を持ち、posts が user_id を持つ。Through は「その2本の外部キーを連鎖でたどる」ショートカットです。
関係の図
countries
Country
- id
- name
users
User(中間)
- id
- country_id→ countries.id
posts
Post
- id
- user_id→ users.id
- title
- users.country_idcountries.id1 — 多— 中間 users が countries を指す
- posts.user_idusers.id1 — 多— posts が中間 users を指す(この2本を連鎖してたどる)
モデル定義コード
Country モデル(User を経由して Post へ)
class Country extends Model
{
// 国 → (users を経由) → その国のユーザーの全投稿
public function posts()
{
return $this->hasManyThrough(
Post::class, // 最終的に欲しいモデル
User::class // 中間のモデル
// 省略時: users.country_id と posts.user_id を推測して連結
);
}
}使い方コード
国から一気に投稿を取得
$country = Country::find(1);
foreach ($country->posts as $post) {
// その国に属する全ユーザーの投稿が、User を書かずに取れる
echo $post->title;
}具体例
「日本のユーザーが書いた投稿を全部集めたい」ようなケース。本来は 国→ユーザー→投稿 と2段たどる必要がありますが、hasManyThrough なら Country::find(1)->posts で一気に取得できます。1件だけ欲しいときは hasOneThrough を使います。
ポリモーフィック(morphOne / morphMany / morphTo / morphToMany)
1つの子テーブルが、複数種類の親に属せる関係。「コメントが記事にも動画にも付く」を、テーブルを増やさず1つでまかないます。
いつ使うか
同じ子(コメント・画像・いいね など)を、複数の異なる親モデルにぶら下げたいとき。親ごとに comments テーブルを分けずに済みます。
外部キーの向き
子テーブルが「相手のID(commentable_id)」と「相手の種類(commentable_type)」の2列を持ちます。type にモデルのクラス名(例: App\\Models\\Post)が入るので、同じ外部キーで複数の親を指し分けられます。親側が morphMany/morphOne、子側が morphTo です。
関係の図
posts
Post
- id
- title
videos
Video
- id
- url
comments
Comment
- id
- body
- commentable_id相手のID
- commentable_type相手のクラス名
- comments.commentable_*posts.id多 — 1— type が Post なら posts を指す
- comments.commentable_*videos.id多 — 1— type が Video なら videos を指す(1つのFKで指し分け)
モデル定義コード
子: Comment(morphTo)
class Comment extends Model
{
// このコメントが「何に」付いているか(Post か Video か…)
public function commentable()
{
return $this->morphTo();
// commentable_id と commentable_type の2列を使う
}
}親: Post / Video(morphMany)
class Post extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
class Video extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}使い方コード
親からコメントをたどる / 子から親をたどる
$post = Post::find(1);
foreach ($post->comments as $c) { // 記事のコメント
echo $c->body;
}
$comment = Comment::find(5);
$parent = $comment->commentable; // Post か Video が返る(実行時に決まる)
// 追加も同じ書き方で親の種類を問わない
$video->comments()->create(['body' => '面白い!']);多対多版: morphToMany(タグを何にでも付ける)
class Post extends Model
{
// タグを Post にも Video にも付けられる多対多ポリモーフィック
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
// 中間テーブル taggables(tag_id, taggable_id, taggable_type)を使う具体例
コメント機能を記事(Post)にも動画(Video)にも付けたいケース。comments テーブルを1つだけ作り、commentable_id と commentable_type の2列で「どの種類の・どのレコードに」付くかを記録します。morphToMany を使えば、タグのような多対多も同じ考え方で複数の親に共有できます。
N+1問題 と Eager Loading(with())
リレーションは便利ですが、うっかり書くと「無駄なSQLが大量に飛ぶ」N+1問題を起こします。リレーションを学んだら必ずセットで覚えたい、最重要の注意点です。
何が問題か
一覧(N件)をループしながら、各件で関連データを $post->user のように取り出すと、1件ごとに追加のSQLが飛びます。最初の一覧取得1回 + N件ぶんのN回 = 合計 N+1 回のクエリになる、これがN+1問題です。件数が増えるほど遅くなります。
解決策
あらかじめ with('リレーション名') で関連データをまとめて先読み(Eager Loading)します。Laravelは関連を IN 句でまとめて取得するので、クエリは合計2回(一覧1回 + 関連1回)で済みます。
悪い例: N+1が発生する
$posts = Post::all(); // クエリ1回
foreach ($posts as $post) {
echo $post->user->name; // 1件ごとに users への追加クエリ!
}
// posts が100件なら 1 + 100 = 101 回のクエリループの中で $post->user を初めて触るたびに、その都度SQLが実行されてしまいます。
良い例: with() で先読み
$posts = Post::with('user')->get(); // 一覧1回 + users まとめて1回 = 2回
foreach ($posts as $post) {
echo $post->user->name; // 追加クエリは飛ばない
}
// 複数・ネストもまとめて指定できる
Post::with(['user', 'comments.user'])->get();with() で「これから使う関連」を宣言しておくと、Laravelがまとめて取得してくれます。件数が増えてもクエリ回数は一定です。
- 遅延読み込みでも load('user') で後から先読みできます(取得済みのモデルに関連を足す)。
- 件数だけ欲しいなら withCount('comments') を使うと $post->comments_count で取れ、これもN+1を防げます。
- 開発中は Laravel Debugbar や DB::listen でクエリ数を見張ると、N+1に気づきやすくなります。
モデルの中身は全体マップの「Eloquentモデルクラスの中身」ツリーも参考になります。用語は実務用語集でも確認できます。