Eloquent / Active Record のリレーション整理(重要6種類)
はじめに
転職をきっかけに、それまで主に使っていたLaravelのQuery Builder中心の書き方から、Eloquent や Ruby on Rails のActive Recordにも触れるようになりました。
テーブル同士の関係を「リレーション」としてモデルに定義することで、SQLを書かずに直感的にデータを扱えることは理解していましたが、実務で触れる中でその意味合いを改めて整理する機会になったので本記事では、その中でも特に重要な6つのリレーションを備忘録として整理していきます。
① hasMany(1が複数持つ)
例
親:ユーザー
子:投稿(複数)
User(1)
└ Post(多)
意味
1人のユーザーが複数の投稿を持つ(1つの親が複数の子を持つ形)
使い方
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
$user->posts;
- Userモデルに「postsという関連がありますよ」と教えているだけ
- hasManyはPostテーブルを user_id で関連付けているだけ
「user_idなんて指定してないじゃん」と最初自分は思ったのですがLarabelやRails(上記はLaravel)が命名規約から自動推測しているから正常に動いています。(以下の流れ)
Laravel/Railsが親モデル名を見る
User
↓
snake_case に変換して _id を付与
user_id
↓
Postテーブル側にそのカラム名があると仮定する
posts.user_id
② belongsTo(多が1に属する)
例
子:投稿
親:ユーザー
Post(多)
└ User(1)
意味
子が親に属している
使い方
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
$post->user;
belongsToを最初見た時に①のhasManyと関係性の違いがわからなかったのですが、
実は同じ関係を逆から見てるだけです。
User視点(hasMany)
「このユーザーの投稿一覧を見たい」
なら$user->posts
Post視点(belongsTo)
「この投稿を書いたユーザーを知りたい」
なら$post->user
となるのでイメージ的には「どのモデル視点で関連を辿りたいか」で使い分けます。
③ hasOne(1対1)
例
親:ユーザー
子:プロフィール
User(1)
└ Profile(1)
意味
1つの親に対して1つだけ存在する
使い方
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}
$user->profile;
構造だけで見るとは①の hasMany とかなり似ています。
違いは取得対象が複数か単数かだけの違いとなります。
※ちなみにORM上の「関係定義」なだけで「DB制約」ではないので hasOne で定義していて対象が複数存在する場合はヒットした最初の1件だけ返る形になるので注意が必要です。
④ belongsToMany(多対多)
例
ユーザー
ロール
※お互いが複数のデータを持てる関係のため、親子ではなく「多対多」の関係となる。
User ←→ Role
中間テーブル:
user_role
意味
お互いが複数持つ関係
使い方
class User extends Model
{
public function roles()
{
return $this->belongsToMany(Role::class);
}
}
$user->roles;
今までは親 → 子でしたが、
User ←→ Role で上下関係がありません。
今回はUser視点で「このUserが持っているRole一覧」を取得する例ですが、
逆に Role視点 で「このRoleを持っているUser一覧」を取得する際には Roleモデル側にbelongsToManyの記載が可能です。
⑤ hasManyThrough(中継すっ飛ばして一覧)
例
親:国
中間:ユーザー
子:投稿(複数)
Country
└ User
└ Post
意味
中継(中間テーブル)を飛ばして一気に取得する(複数件)
使い方
class Country extends Model
{
public function posts()
{
return $this->hasManyThrough(Post::class, User::class);
}
$country->posts;
}
// 取得時イメージ
$country = Country::find(1);
$country->posts;
普通ならCountry → User → Post でUserテーブルを経由しないといけませんが hasManyThroughを使うと $country->posts で直接取得ができます。
※補足
[以下DBイメージ]
countries
id
users
id
country_id
posts
id
user_id
上記構成の時にcountries視点で日本の投稿だけを取得したい時に User を経由しなくてはいけないのでhasManyThrough(Post::class, User::class);で第一引数に最終的に取得したいモデルを指定、第二引数に途中で経由するモデルを指定することで自身でJoinなど記述することなく取得ができている。
※$country->postsで書くのでUserを経由してなさそうだが実際には以下のようなSQLが内部で流れている。
SELECT posts.*
FROM posts
JOIN users
ON users.id = posts.user_id
WHERE users.country_id = 1
⑥ hasOneThrough(中継すっ飛ばして単体)
例
親:ユーザー
中間:アカウント
子:パスポート
User
└ Account
└ Passport
意味
中間を1つだけ経由して単一データを取得
使い方
class User extends Model
{
public function passport()
{
return $this->hasOneThrough(Passport::class, Account::class);
}
}
考え方は hasManyThrough と同じです。
違いは複数取るか1件取るかになります。
Through系 は「本当は中間モデルを辿る必要があるものを、省略して直接取得する」で覚えておくとわかりやすいと思います。(Throughの意味: 〜を経由して / 通して)
まとめ
まずは今回紹介した6つを理解しておくと、実務でも困りにくくなると思います。
他にも、複数のモデルを柔軟に関連付けられる「ポリモーフィックリレーション」なども存在しますが、それはまた別の記事で整理したいと思います。
※以下確認用一覧表
| リレーション | 関係 | イメージ |
|---|---|---|
| hasMany | 1対多 | 1が複数持つ |
| belongsTo | 多対1 | 多が1に属する |
| hasOne | 1対1 | 1つだけ持つ |
| belongsToMany | 多対多 | お互い複数持つ |
| hasManyThrough | 間接1対多 | 中間を経由して複数取得 |
| hasOneThrough | 間接1対1 | 中間を経由して単体取得 |
Discussion