Laravel Eloquentの魅力の一つは、テーブル同士の関連をシンプルかつ明快に表現できる「リレーション」機能です。しかし、実際に使ってみると「どっちが hasMany?」「外部キーってどっち側に持たせるんだっけ?」といった疑問が絶えないのも事実。
本記事では、Eloquentのリレーションについてあえて丁寧に分解し、初心者がつまずきやすいポイントを一つずつ解説していきます。「もしかすると知ってるつもりで曖昧かも…」という方は、再確認にぜひ役立ててください。
- 1. hasMany/hasOne vs. belongsTo──「親子関係」と「外部キー」で考える
- 1.1. 例1: User と Post
- 1.2. 例2: User と Profile
- 2. 一方だけ書いても動くけど、両方書くほうがメリット大
- 3. なぜ混乱しやすい?──「現実世界のイメージ」と「データベース実装」のギャップ
- 4. コード例で確認する ── hasMany/hasOneとbelongsToの組み合わせ
- 5. Eloquentのリレーションは命名規則に従えば自動的にキーを推測してくれる
- 6. もし命名が異なる場合は手動指定が必要
- 7. 自動推測が有効な条件
- 8. まとめ
- 9. 関連記事
- 10. FAQ
hasMany/hasOne vs. belongsTo──「親子関係」と「外部キー」で考える
Eloquentでリレーションを定義するときは、「外部キーをどちらが持っているか?」が最重要の視点です。テーブル設計で「どのモデルのIDをどのテーブルに持たせるか」が決まっているはずなので、それに従ってリレーションを設定します。
親モデル(外部キーを持たれ“る”側): hasMany
または hasOne
子モデル(外部キーを“持つ”側): belongsTo
例1: User と Post
- Postテーブルに
user_id
カラムがある場合users
テーブル:親posts
テーブル:子(user_id
が外部キー)
// 親モデル(User.php)
class User extends Model
{
// 「複数の投稿を持つ」= hasMany
public function posts()
{
return $this->hasMany(Post::class);
}
}
// 子モデル(Post.php)
class Post extends Model
{
// 「投稿は1人のユーザーに属する」= belongsTo
public function user()
{
return $this->belongsTo(User::class);
}
}
ここで「どっちに hasMany
を書くか?」で迷う場合は「どのテーブルが外部キーを持っているか」を確認すると分かりやすいです。外部キー(user_id
)を持つのが posts
テーブルなので、「Post は User に属する (belongsTo
)」。したがって、逆側のUserモデルが「Postを複数持つ (hasMany
)」と定義します。
例2: User と Profile
- Profileテーブルに
user_id
カラムがある場合users
テーブル:親profiles
テーブル:子(user_id
が外部キー)
// 親モデル(User.php)
class User extends Model
{
// 「ユーザーは1つのプロフィールを持つ」= hasOne
public function profile()
{
return $this->hasOne(Profile::class);
}
}
// 子モデル(Profile.php)
class Profile extends Model
{
// 「プロフィールは1人のユーザーに属する」= belongsTo
public function user()
{
return $this->belongsTo(User::class);
}
}
「1対1」の場合は hasOne
と belongsTo
の組み合わせになります。これもやはり、外部キーを持っているのはProfileテーブルなので、「Profile は User に属する」という書き方になります。
一方だけ書いても動くけど、両方書くほうがメリット大
Eloquentは片側だけのリレーション定義でも動作自体はします。しかし実務では、「親→子」「子→親」の両方のアクセスが必要になるシーンが多いものです。
- 親モデル(User)から「このユーザーが持つPost一覧を取得したい」
- 子モデル(Post)から「この投稿を書いたユーザーを取得したい」
こうした操作が発生するなら、両方にリレーションを定義しておくほうが便利です。片側のみだと、たとえば $post->user()
といったアクセスができず、結局クエリを直接書く羽目になるかもしれません。
なぜ混乱しやすい?──「現実世界のイメージ」と「データベース実装」のギャップ
Eloquentリレーションが混乱を招く要因の一つは、下記のような「言葉のニュアンス」と「データベースの実装」が噛み合わない場合があるからです。
- 「UserがPostを“持つ”とは言うけど、実際に
user_id
はPost側にあるからイメージと逆に感じる」 - 「
belongsTo
(属する)のほうが外部キーを持つのか…?」
ここで決め手になるのは、あくまで「どのテーブルに外部キーが存在するか」という事実です。Eloquentの定義はその事実に基づいているため、「belongsTo = 外部キーを持っている側」「hasMany/hasOne = 外部キーを持たれている側」という形になります。
コード例で確認する ── hasMany/hasOneとbelongsToの組み合わせ
多対一(1人のユーザーが複数の投稿を持つ)
// User(親):hasMany
public function posts()
{
return $this->hasMany(Post::class);
}
// Post(子):belongsTo
public function user()
{
return $this->belongsTo(User::class);
}
一対一(1人のユーザーに対して1つのプロフィール)
// User(親):hasOne
public function profile()
{
return $this->hasOne(Profile::class);
}
// Profile(子):belongsTo
public function user()
{
return $this->belongsTo(User::class);
}
どちらのパターンでも、“親”となるモデルが has___
系、“子”となるモデルが belongsTo
という組み合わせです。
Eloquentのリレーションは命名規則に従えば自動的にキーを推測してくれる
LaravelのEloquentはデフォルトで「モデルの主キー=id
」「外部キー=モデル名(スネークケース)_id
」という命名規則を前提にしているため、以下のようにシンプルに書くだけでキーを設定しなくても動作します。
public function posts()
{
// デフォルトで「Postテーブルの user_id を使い、Userモデルの id を参照」
return $this->hasMany(Post::class);
}
public function user()
{
// デフォルトで「Userテーブルの id と、Postテーブルの user_id を関連付け」
return $this->belongsTo(User::class);
}
このように「クラス名が User → テーブル名が users → 外部キーが user_id → 親のPKは id」という形で一致していれば、キーの指定を省略してOKです。
もし命名が異なる場合は手動指定が必要
デフォルトの命名規則と異なるスキーマを使っている、あるいは主キーを id
以外の名前にしている場合は、手動でキーを指定する必要があります。たとえば外部キーが author_id
で、Userモデルの主キーが user_uuid
というようなケースです。
public function posts()
{
// 第2引数が「外部キー名」、第3引数が「親の主キー名」
return $this->hasMany(Post::class, 'author_id', 'user_uuid');
}
自動推測が有効な条件
- テーブル名: 通常は「モデル名をスネークケース+複数形」
- 例:
User
→users
,Post
→posts
- 例:
- 外部キー: 「親モデル名(スネークケース)_id」
- 例:
Post
テーブル側がuser_id
を持つ場合、belongsTo(User::class)
は自動でuser_id
を探す
- 例:
- 主キー: 通常は
id
- 例: 親の主キーも
id
であれば、追加指定なしでOK
- 例: 親の主キーも
上記のように、Eloquentのデフォルト命名規則に則ったテーブル設計をしている限り、リレーション定義時にキーを明示しなくても自動で解決してくれます。
まとめ
- 「
id
」を主キーにし、「モデル名_id
」を外部キーにしているなら、キー設定を省略してOK。 - 命名規則と異なる場合は、「
->hasMany(Post::class, '外部キー', '主キー')
」や「->belongsTo(User::class, '外部キー', '親の主キー')
」のように指定が必要。 - プロジェクト規模が大きくなると「既存のテーブル名やキー名が揃わない」こともあり、そういったときはしっかりキーを指定してEloquentに教えてあげるのがベスト。
Laravelでは規約優先(Convention over Configuration)の設計が多いので、命名を合わせるだけで自動的に動くのはかなり大きなメリットです。もし混乱する場合は「外部キーを持つのはどっち?」「親の主キーは何?」を改めてテーブル定義と照らし合わせながら確認するとスッキリ理解できるでしょう。
関連記事
LaravelのEloquentは知ってる。でも“理解してる”って言えるのか?
FAQ
-
hasMany / hasOne と belongsTo、どちらを“子モデル”に書くの?
-
postsテーブルに
user_id
があれば Post モデルが belongsTo(User)、
User モデルは hasMany(Post) / hasOne(Profile) になります。
「外部キー ⇒ belongsTo」と覚えると迷いません。
-
リレーションは片側だけ定義しても動く?
-
動作はしますが 実務では両方向を定義 したほうが便利。
片側だけだと$post->user
や$user->posts
のどちらかが書けず、
結局クエリを手書きする羽目になります。
-
外部キー名・主キー名が規約と違う場合は?
-
第2引数に外部キー、第3引数に関連先の主キーを渡します。
return $this->hasMany(Post::class, 'author_id', 'user_uuid');
-
中間テーブルを使う多対多はどう書く?
-
belongsToMany
を両モデルに定義し、
中間テーブル名・外部キーが規約どおりなら引数はモデル名だけでOK。public function roles() { return $this->belongsToMany(Role::class); }
-
withTrashed()
やsoftDeletes()
はリレーション側にも必要? -
親 or 子どちらか一方をソフト削除にしている場合、
リレーション定義の末尾にwithTrashed()
を付けると
論理削除済みの行も取得できます。return $this->hasMany(Post::class)->withTrashed();
-
load()
とwith()
の違いは? -
with() → クエリビルド時に eager load。
load() → 取得済みのモデルに対してあとから eager load。
大量データを扱う際は with() でまとめて取得するのがパフォーマンス的に有利です。
-
パフォーマンスが気になる。N+1 を防ぐ方法は?
-
ループ処理の中でEloquentを利用しないを意識しててもEagerLoadを忘れがち。
with()
/load()
を使った eager load を使う。
加えてselect()
でカラムを絞る、orderBy
やlimit
をサブクエリ側に書く などで負荷を抑えます。
-
リレーションの結果をさらに加工したいときは?
-
with()
にラムダ式入れてクエリ追加できたりします。->with(['posts' => function ($q) { … }])
コメントを残す