大前提
テーブル名やカラム名の命名規則は遵守すること。
- プライマリは
id
-
他テーブル名_id
は必ず外部キー。
論理名として「なんとかID」があったとしても、nantoka_key
や nantoka_code
にし、存在しないテーブルの naktoka_id
は絶対に作ってはならない。
なお、ここで「外部キー」は必ずしも外部キー制約(FORREIGN KEY)を振るものではない。Eloquet のリレーションを結びつける論理キーを指している。
詳しくは、Eloquent 前提の MySQL データベース設計 を参照のこと。
リレーション定義の指針
- モデル定義と同時にすべての定義を書く必要はなく、コーディングの都度追加で十分。
- すなわち、hasMany に対応する belongsTo を必ずしもペアで定義する必要性はない。
- 慣れれば定義すべきリレーションは想像つくので、真っ先に定義することを否定はしない。
- 関係を決めるときに、「1対1か、1対多か」の呪縛 から抜け出すこと、これ大事。
- 最初に考えるのは外部キーを誰が持っているか。相手が持つときはじめて1か多か決める。
自分 | 他テーブル | リレーション定義 | |
---|---|---|---|
👱🔑 | 🙋 🙅 | 相手のキーを自分が持っているなら belongsTo | |
👱 | 🙋🔑🙅 | 自分のキーを一人にだけわたすなら hasOne | |
👱 | 🙋🔑🙋🔑 | 自分のキーを複数人にわたすなら hasMany | |
🙋 | 🔑🔑 | 🙋 | 中間テーブルでキーを持ち合うなら belongsToMany |
※「人」と書いているが他テーブルは人とは限らない。
- リレーション名は接頭語や接尾語を(あるなら)省き、シンプルな論理名にする。
- 例えば、Project から ProjectGroup へのリレーション名なら、group にする。
- hasOne と belongsTo は単数形、hasMay や belongsToMany は複数形とする。
第2引数と第3引数の覚え方
テーブル名やカラム名の命名規則を遵守していれば、基本的なリレーション定義に第2引数以降は不要。
return $this->blongsTo(Company::class);
外部キーが 相手のテーブル名(単数形)_id でないとき、第2引数を使う。よくあるのは、階層や親子関係のため自分自身にリレーションするとき、テーブル名ではなく parent_id を使うことがある。
return $this->belongsTo(self::class, 'parent_id');
プライマリキーを id 以外にしてしまうと第3引数の登場となる。第2引数は 相手テーブル名(単数形)_id で規定通りなのだが、第3引数があるので省略できない。こうなると、どちらが何だかわからないので命名規則は重要なのだった。
return $this->belongsTo(Company::class, 'company_id', 'company_id');
多対応のリレーションには pluck と flatten
例えば、ユーザーが所属会社を横断したプロジェクトに属していて、ユーザーとプロジェクトが多対多対応 となる場合で、ログインユーザーと関わりのあるプロジェクトの会社名をすべて取得することを考えてみる。
会社とプロジェクトには直接的な結びつきはなく、間にユーザーを介した関係となっているものとする。
練習用のプロジェクトを実際に作成するには 前記事 を参照。
各モデルに定義するリレーションは次のようなものになる。
App\Models\User
public function company()
{
return $this->belongsTo(Company::class);
}
public function projects()
{
return $this->belongsToMany(Project::class, 'user_project');
}
App\Models\Project
public function users()
{
return $this->belongsToMany(User::class, 'user_project');
}
欲しいのは、ユーザー ⇒ プロジェクト ⇒ ユーザー ⇒ 会社(⇒ 会社名) というリレーションだ。
直接リレーションしているモデル同士なら、関係を記述することは難しくない。
実際にこのようなデータベース定義とモデル定義を作成できたなら、tinker で結果を確認してみよう。
ユーザー ⇒ プロジェクト、 および ユーザー ⇒ 会社
$user = User::find(1);
$user->projects; // Project のコレクション
$user->company; // Company オブジェクト
プロジェクト ⇒ ユーザー
$project = Project::find(1);
$project->users; // User のコレクション
会社 ⇒ 会社名
$company = Company::find(1);
$company->name; // 文字列
では、これらをつなげるにはどうしたらよいだろうか。
ユーザー ⇒ プロジェクト ⇒ ユーザ を単純につなげるとエラーになる。
$user = User::find(1);
$user->projects->users; // エラー
これは $user->projects がコレクションだからで、コレクション内の1つずつを取り出さなければ次につながらないからだ。
コレクションから次につなげるには pluck を用いる。
$user = User::find(1);
$user->projects->pluck('users'); // User のコレクション?
では、そこから会社につながるだろうか?
$user = User::find(1);
$user->projects->pluck('users')->pluck('company'); // ???
エラーにはならないが、結果は空となる。
tinkerで確認すると pluck('users') は コレクションのコレクション となっていたことがわかる。
>>> $user->projects->pluck('users');
=> Illuminate\Support\Collection {#4324
all: [
Illuminate\Database\Eloquent\Collection {#4440
all: [
App\Models\User {#4386
id: 2,
flatten で平坦なコレクションに変換すれば pluck がつながるようになる。
$user = User::find(1);
$user->projects->pluck('users')
->flatten()->pluck('company'); // Company のコレクション
ということで、会社名文字列のコレクションを得るのは次で正解。
リレーション名の命名規則(複数形と単数形)がとっても大事だった。
$user = User::find(1);
$names = $user->projects->pluck('users')
->flatten()->pluck('company')
->pluck('name')->unique();
だが、ここまでの知識でリレーションを駆使すると 「N+1問題」 が発生し、大量の SQL クエリを発行してしまい、「Eloquent は遅い! ビルダーで生クエリを書くほうが良いのだ」という主張が出てきてしまう。
「N+1問題」とは、ループの中で SQL クエリ発行をくり返すこと。長くなるので次の記事に。