Eloquentの多対多のリレーションについてです。

多対多というのは、例えばポストとタグのような関係のことです。あるひとつのポストは複数のタグを持ち、あるひとつのタグは複数のポストで使われるといった感じです。

ということなので、今回はポストとタグのリレーションを想定しています。

言葉だけではわかりづらいと思うので、ER図やDBのレコードも交えながら書いていきます。

環境

  • Laravel 5.4.23
  • MySQL 5.7.16 (InnoDB)

テーブル

まずはテーブルですが、多対多のリレーションでは公差テーブルが必要になります。なので、ポスト(posts)、タグ(tags)、公差テーブル(post_tag)の3つを作成します。

ER図はこんな感じになります。

多対多のER図

公差テーブルを作成する上で注意するのがテーブル名です。日本語ドキュメントにはこう書かれています。

多対多の関係はhasOneとhasManyリレーションよりも多少複雑な関係です。このような関係として、ユーザ(user)が多くの役目(roles)を持ち、役目(role)も大勢のユーザ(users)に共有されるという例が挙げられます。たとえば多くのユーザは"管理者"の役目を持っています。users、roles、role_userの3テーブルがこの関係には必要です。role_userテーブルは関係するモデル名をアルファベット順に並べたもので、user_idとrole_idを持つ必要があります。

引用の最後の文に注目してください。関係するモデル名をアルファベット順に並べるとあります。そしてアンダースコアで繋げたものがテーブル名になります。従って、今回必要な公差テーブルの名前はpost_tagとなります。

また、user_idとrole_idが必要とありますが、これは外部キーのことです。今回必要なのは、post_idとtag_idですね。

以上を踏まえてモデルとマイグレーションファイルを作成します。

$ php artisan make:model Post -m
$ php artisan make:model Tag -m

post_tagテーブルはマイグレーションファイルのみでモデルは作成しません。

$ php artisan make:migration create_post_tag_table

[postsテーブル]

[tagsテーブル]

[post_tagテーブル]
今回は2つの外部キーを複合主キーとしています。

テーブルが完成したところで、シーダーを使ってデータを入れます。

書き終われば、マイグレーションとシーダーを実行します。

$ php artisan migrate --seed

公差テーブルのシーダーが読みにくいとは思いますが、結果として以下のようになればOKです。

postsテーブル

tagsテーブル

post_tagテーブル

モデル

最初に、ひとつのポストが多くのタグを持つリレーションです。

新しく追加したtags()belongsToMany()を返すようにします。引数はタグモデルです。

ルーティングとコントローラーとビュー

例えば、localhost:8000/postsというURLにアクセスした時にポストとそれぞれのポストが持つタグを一覧で表示するとします。

[ルーティング]

[コントローラー]

[ビュー]

とても簡単な記述ですが、以下のように表示されます。

ポストとそれぞれのポストが持つタグの一覧

逆のリレーション

では次に逆のリレーションについても書いていきましょう。

逆のリレーションは、あるタグに紐づけられたポストになります。

[タグモデル]
タグモデルもポストモデルと同様にbelongsToMany()を返すメソッドを定義します。

URLはlocalhost:8000/tagsにし、それぞれのタグに紐づけられたポストも一緒に表示してみましょう。

[ルーティング]

[コントローラー]

[ビュー]

こちらも問題ないですね。

タグとタグに紐づけられたポストの一覧

Eager Loading

これまでの書き方でも問題ないですが、通常リレーションをプロパティとしてアクセスする場合は遅延ロード(Lazy Loading)されます。プロパティにアクセスするまではリレーションのSQLは発行されないということです。

ただし、N + 1問題があります。例えば、ポストに紐づくタグを取得した時のSQLは次のようになります。

N + 1問題のSQL

SQLが6個発行されていることがわかります。これは、全5個あるポストを取得するためと各ポストに紐づくタグ取得のためです。ポストが5個ではなく10個だったら11個のSQLが発行されることになります。

これはボトルネックになりかねません。

そこでEager Loadingです。これはリレーション元のモデル取得直後に関連するモデルを一気に取得するのです。上記の場合だと、全5個あるポストを取得した直後にそれらに関連するタグも取得することになります。

Eager Loadingはwith()を使います。with()の引数は関連するテーブル名になります。

Eager Loadingに書き直したコントローラーがこちらです。

表示は変わりませんが、結果として発行されるSQLは2個になります。

postsのEager LoadingのSQL

tagsのEager LoadingのSQL

まとめ

基本的な多対多のリレーション定義でした。1対多と同じくらいよく使われますのでよく理解しておくといいと思います。