はじめに
2025年4月からWebエンジニアとして働き始めて、Laravelのリレーションでつまづいた部分を整理しました。同じように悩んでいる初心者の方の参考になれば嬉しいです!
この記事の内容
- リレーションの4つの基本パターンと使い分け
- 実際のコード例とマイグレーション
- 開発中によくある疑問の解決方法
- 実践で役立つTips
リレーションとは
データベースのテーブル同士のつながりのこと
例:「ユーザーと投稿」の関係
- ユーザー(usersテーブル)
- 投稿(postsテーブル)
→ 「1人のユーザーは複数の投稿を持つ」= 1対多の関係
リレーションの種類
データベースのリレーションは以下の4パターンに分類される
| 関係 | 説明 | Laravel | 外部キー |
|---|---|---|---|
| 1対1 | 一つのレコードが別テーブルの一つのレコードと対応 |
hasOne / belongsTo
|
片方が持つ |
| 1対多 | 一つのレコードが別テーブルの複数レコードと対応 |
hasMany / belongsTo
|
「多」側が持つ |
| 多対1 | 1対多の逆方向(書き方が異なるため区別) |
belongsTo / hasMany
|
「多」側が持つ |
| 多対多 | 複数のレコードが複数のレコードと対応 | belongsToMany |
中間テーブル |
** 重要なポイント:**
-
外部キー(FK)を持つ側 →
belongsTo() -
外部キーを持たれる側 →
hasOne()またはhasMany()
1対1の関係(hasOne)
例:ユーザーとプロフィール
- 1人のユーザーは1つのプロフィールを持つ
- 1つのプロフィールは1人のユーザーにだけ対応する
テーブル構造
-- usersテーブル
users: id, name, email
-- profilesテーブル
profiles: id, user_id(FK), bio, avatar
マイグレーション例
// create_profiles_table.php
Schema::create('profiles', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->text('bio')->nullable();
$table->string('avatar')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
モデルの定義
// 🗂️ Userモデル(app/Models/User.php)
class User extends Authenticatable
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}
// 🗂️ Profileモデル(app/Models/Profile.php)
class Profile extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
使用例
$user = User::find(1);
echo $user->profile->bio; // プロフィール情報にアクセス
$profile = Profile::find(1);
echo $profile->user->name; // 逆方向のアクセス
1対多の関係(hasMany)
例:ユーザーと投稿の関係
- 1人のユーザーは複数の投稿を持つ
- 1つの投稿は1人のユーザーに属する
テーブル構造
-- usersテーブル
users: id, name, email
-- postsテーブル
posts: id, user_id(FK), title, content
モデルの定義
// 🗂️ Userモデル
class User extends Authenticatable
{
public function posts()
{
return $this->hasMany(Post::class); // 1対多
}
}
// 🗂️ Postモデル
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class); // 多→1の逆方向
}
}
使用例
$user = User::find(1);
foreach($user->posts as $post) {
echo $post->title; // そのユーザーの投稿一覧
}
$post = Post::find(1);
echo $post->user->name; // 投稿者名
多対多の関係(belongsToMany)
例:投稿とタグ
- 1つの投稿に複数のタグが付けられる
- 1つのタグが複数の投稿に使われる
なぜ中間テーブルが必要なのか?
❌ 直接関係を作ろうとした場合の問題
-- これだと1つの投稿に1つのタグしか付けられない
posts: id, title, tag_id
-- これだと1つのタグに1つの投稿しか紐づかない
tags: id, name, post_id
⭕️ 中間テーブルで解決
posts: id, title
tags: id, name
post_tag: post_id(FK), tag_id(FK) -- 中間テーブル
マイグレーション例
// create_post_tag_table.php(中間テーブル)
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('post_id');
$table->unsignedBigInteger('tag_id');
$table->timestamps();
$table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
});
モデルの定義
// 🗂️ Postモデル
class Post extends Model
{
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}
// 🗂️ Tagモデル
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); // タグを追加
$post->tags()->detach($tagId); // タグを削除
$post->tags()->sync([$tag1, $tag2]); // 指定したタグのみに同期
自分の開発中に出てきた疑問
Q1. 多対1の場面で「多」の方が主テーブルになることってないのかな?
A: あった!重要なのは「どちらが外部キー(FK)を持つか」ということ
例:投稿とカテゴリー
// 📝 投稿が外部キーを持つパターン(一般的)
// posts: id, title, category_id(FK)
// categories: id, name
// Postモデル
public function category()
{
return $this->belongsTo(Category::class); // 外部キーを持つ側
}
// Categoryモデル
public function posts()
{
return $this->hasMany(Post::class); // 外部キーを持たれる側
}
覚え方:
-
外部キー(FK)を持つ方 →
belongsTo() -
外部キーを持たれる方 →
hasOne()/hasMany()
Q2. 中間テーブルって何?多対多だけに出てくるのか?
A: 中間テーブルは2つのテーブルの「多対多関係」を管理するための「橋渡し役」テーブル!
中間テーブルの特徴
- 多対多のときだけ必要になる
- 中間テーブル自体は「モデルを作らない」ことが多い(Laravelが自動で扱う)
- 命名規則:アルファベット順の単数形をアンダースコアで結合(
post_tag)
なぜ必要なのか?
- 多対多の関係を直接表現することができないため、中間テーブルで「どの投稿にどのタグが付いているか」を管理する
Q3. なんで関数名が紐付ける先のテーブル名なのか?なんで関数に書くのか?
A: これはLaravelの「Eloquent ORM」の仕組み
関数名は「関連付ける先の名前」にするのがLaravelの慣習らしい
// Postモデル内
public function user()
{
return $this->belongsTo(User::class);
}
ここでuser()って名前にすることで
$post = Post::find(1);
echo $post->user->name; // 自然にアクセス可能!
もしも関数名を getUserInfo() とかにすると、$post->getUserInfo になってしまって、ORMの便利機能と連携しにくくなる。。。!
関数なのに自動で実行されるように見える理由
これはLaravelが「マジックメソッド」で内部処理してくれるから
$post->user // ←これを書いた時
Laravelが内部で自動的に
$post->user() // ←関数を呼ぶ!
->getResults(); // ←中で自動的に実行
ってやってくれるらしい。なのでJavaScript的な感覚では「関数呼んでないのに?」ってなりますが、実際はLaravelが勝手にやってくれているだけでした。
まとめ
Laravelのリレーションは最初は複雑に感じました。。。なんで内部的に勝手に紐づくのか、みたいなことが多く癖があるなという印象でした。
ただ、リレーションに関しては以下のポイントを押さえれば理解しやすくなりました
- 外部キーを持つ側が
belongsTo() - 関数名は関連付ける先の名前にする
- 多対多は中間テーブルが必要
実際にコードを書きながら覚えていくのが一番かなと思いました。
同じように悩んでいる方の参考になれば嬉しいです!