【Laravel】Eloquent 多対多について

こんにちは。1月末ほどから弊社webチームに参加した黒岩と申します。

記念すべき初執筆ということで何を書こうか迷ったのですが、
個人的に気になったというか留めておきたいと思ったことがあったので、
今回は、Laravelでの複数テーブル相互関係の一つ。
多対多のリレーションについて解説します。

前提条件

  • LaravelのMVCについて理解がある
  • Laravelのコントローラーを触ったことが多少ある

多対多とは

「多対多」の関係って例えばどんなものがあるんだろう?と思ったときに今回題材に上げたいのが、ブログやSNSです。

ブログやSNSには、「タグ機能」というものがあると思います。
記事に複数のタグをつけることで、検索しやすくなるあれですね。

このように、
一つのタグに不特定多数の記事が関連付けられている
一つの記事には複数のタグが付けられている

この機能を実現するには、多対多の関係を持つテーブル群は不可欠なのです。

多対多のテーブル構成

ブログやsnsのタグ機能実現のためには、最低3つのテーブルが必要です。

  • 記事のテーブル
  • タグのテーブル
  • 記事とタグの関連テーブル(中間テーブル)

今回の多対多を語る上で必須なのが、
中間テーブル」というものです。

なぜそのようなものが必要なのか?
先ほど記述したように、一つの作品には複数のタグが付き、一つのタグは複数の作品につけられます。

それなら以下のようにすればいいんじゃないの?と思った人ももしかしたらいらっしゃるかもしれません。

しかしこれだとDBとして避けなければならない以下の状況に陥ってしまいます。

  • 主キーが重複してしまう
  • 一つのカラムに複数の値を入れないといけなくなってしまう

一つずつ説明していきましょう。

主キーが重複してしまう

タイトル: 日記
内容: くぁwせdrftgyふじこlp

この記事があったとします。これを上のテーブルで表現しようとすると主キーが被ったテーブルができてしまいます。

主キーというのは当たり前ですが重複厳禁のカラム。重複しないようにしたとしてもテーブル構成として望ましくありません。

一つのカラムに複数の値が入ってしまう

もしくは一つのカラムに複数の値を入れざるを得ません。

上のテーブルでもやり方次第では「split(',')」などを使えばデータを正しく取り出せるかもしれませんが、外部キーなので型判定などが難しくなってしまいます。

これらのような事態を避けるべく、中間テーブルは必須ということです。

記事とタグの関係を中間テーブルを用いて表した図

多対多リレーションをLaravelで扱う

ここからLaravelでリレーションをどう扱うのかについて見ていきます。

blogs、tagsスキーマは以下の通りです。 blogs

public function up()
{
    Schema::create('blogs', function (Blueprint $table) {
        $table->id();
        $table->string('blog_title');
        $table->text('blog_content');
        $table->timestamps();
    });
}

tags

public function up()
{
    Schema::create('tags', function (Blueprint $table) {
        $table->id();
        $table->string('tag_name');
        $table->timestamps();
    });
}

記述

上記で使用した注文と商品を題材に考えます。
先に説明したように「多対多」は「1対多」と「1対多」の関係なので、Laravelで「1対多」を表すBelongsToManyメソッドを使用します。

use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Blog extends Model
{
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class, 'blog_tags');
    }
}

それでは、中間テーブルを作成していきます。
以下のコマンドを使用しマイグレーションファイルとモデル(BlogTag)を作成します。

php artisan make:model BlogTag -m

:::info
make:modelコマンドの -m オプションでmigrationファイルも同時に作成できます。
:::

blog_tagsテーブルのスキーマ定義はこのようにしておきます。

public function up()
{
    Schema::create('blog_tags', function (Blueprint $table) {
        $table->id();
        $table->foreignIdFor(Blog::class)->constrained();
        $table->foreignIdFor(Tag::class)->constrained();
    });
}

:::info
constrained()
:::

基本的にLaravel(のコマンド)で作られるmigrationファイルのテーブルはデフォルトでは複数形(blog_tags)です。

しかしBelongsToManyでLaravelが想定しているテーブルは単数形(blog_tag)なので、ここに一手間加える必要があります。

マイグレーションファイルを編集してテーブルを単数形(blog_tag)にすることも可能ですが、今回はbelongsToManyの第二引数にテーブル名を指定する方法で対処することにします。

use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Blog extends Model
{
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class, 'blog_tags');
    }
}

これでこのように記述することで関連したモデルを取得することができます。

Blog::find(1)->tags;

:::warning
ここで取得されるのはモデル(App\Models\Tag)ではなくコレクション
(Illuminate\Database\Eloquent\Collection)です。
:::

逆も同じでTagblogsメソッドを追加することでtag側からも関連したモデルを取得することができます。

use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Tag extends Model
{
    public function blogs(): BelongsToMany
    {
        return $this->belongsToMany(Blog::class);
    }
}
Tag::find(1)->blogs;