Laravel Eloquentリレーション全種類まとめ|hasMany・belongsToの違いと正しい定義方法を徹底解説

基本文法・構文ガイド実装・応用テクニック

Laravelの強力なORMであるEloquentにおいて、データベース操作を圧倒的に直感的かつ効率的にしてくれる機能が「リレーション(Eloquent Relationships)」です。

しかし、開発を始めたばかりの初心者や中級者にとって、以下のような疑問や混乱が頻繁に発生します。

この記事では、LaravelにおけるEloquentリレーションの全種類一覧、最もつまずきやすい hasManybelongsTo の違い・見分け方の鉄則、外部キー・主キーの命名規則とカスタマイズ、中間テーブルの操作(sync / attach / detach)、関連データの取得・保存・更新テクニック、N+1問題への対策まで、実務でそのまま使えるコード例とともに徹底解説します。


  1. 【早見表】Laravel Eloquentリレーション全種類一覧&使い分け
  2. 一番迷う!hasMany と belongsTo の違いと見分け方
    1. 鉄則1:外部キー(user_id等)を持つテーブル側に「belongsTo」を書く
    2. 鉄則2:メソッド名は「hasManyは複数形」「belongsToは単数形」
    3. hasMany と belongsTo の対比コード
  3. Eloquentリレーションの基本:1対1・1対多・多対多の実装例
    1. 1. 1対1リレーション(hasOne / belongsTo)
      1. マイグレーション設計
      2. モデル定義
      3. データの利用
    2. 2. 1対多リレーション(hasMany / belongsTo)
      1. マイグレーション設計
      2. データの利用
    3. 3. 多対多リレーション(belongsToMany)と中間テーブル
      1. 中間テーブルの命名規則
      2. マイグレーション設計
      3. モデル定義(両方に belongsToMany を記述)
      4. 中間テーブルのカスタムカラム(withPivot)
  4. 外部キー・主キーの命名規則とカスタマイズ方法
    1. デフォルトの命名規約
    2. カスタムキーの指定方法
  5. リレーションを使ったデータ操作(取得・登録・更新・削除)
    1. 1. 動的プロパティ vs クエリメソッド
    2. 2. 1対多の登録:save() と create()
    3. 3. 多対多の操作:attach / detach / sync / toggle
    4. 4. belongsTo の更新:associate() と dissociate()
  6. 応用リレーション:Has-Through と ポリモーフィック
    1. 1. 経由リレーション(hasOneThrough / hasManyThrough)
    2. 2. ポリモーフィックリレーション(Polymorphic)
  7. 実務で必須!リレーション利用時の注意点&ベストプラクティス
    1. 1. N+1問題の発生を防止する(with による Eager Loading)
    2. 2. リレーション先の存在チェック・絞り込み(has / whereHas)
    3. 3. 件数のみ必要な場合は withCount() を使う
    4. 4. 複数テーブルへの更新はトランザクションで保護する
  8. 関連記事・内部リンク
  9. まとめ:リレーションを使いこなして美しいEloquent設計を
  10. 関連記事

【早見表】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のリレーションを学ぶ上で、初心者が最初にぶつかる壁がhasManybelongsTo のどちらをどちらのモデルに定義するか」です。

この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つのモデル名を単数形スネークケースにし、アルファベット順にアンダースコアで結合した名前」を中間テーブル名として認識します。

マイグレーション設計

// 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_commentsvideo_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 トランザクションの使い方完全ガイド を参考にしてください。



まとめ:リレーションを使いこなして美しいEloquent設計を

LaravelのEloquentリレーションを理解することで、SQLの記述量を減らしつつ、保守性が高く堅牢なデータベース操作を実現できます。

リレーションのルールと命名規則をマスターし、すっきりと読みやすいLaravelコードを書いていきましょう。

レン (Wren)

こんにちは。レンです。

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

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

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

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

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

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

コメント