リモート開発メインのソフトウェア開発企業のエンジニアブログです

Laravelの多対多のリレーションについて

はじめに

皆さんこんにちは。Laravelの多対多のリレーションについて、曖昧な部分が多かったので勉強がてら基本的な部分を整理してみました。

多対多には中間テーブルとbelongsToMany()を使う

例えば「ブログ記事」とそれに対する「タグ」の場合のように、それぞれのブログ記事は複数のタグを持ち、それぞれのタグも同様に複数のブログを持ち得るような場合で、中間テーブルが必要になるケースです。Laravelにおけるこのような多対多の関係は、belongsToMany()を使って定義されることになります。

belongsToMany()を使って定義した多対多のリレーションの場合、attach()/detach()/sync()などのメソッドを利用することができます。サンプルを使ってコンソールでそれらのメソッドを確認する方法も記載しましたので、参考にしてみてください。

ちなみにhasManyThrough()を使っても例えばブログ記事から紐づくタグを取得するというようなことは可能なようですが、hasManyThrough()は3つのテーブルが介するケースに対するショートカットのようなものに過ぎないようなので、多対多のケースに関してはbelongsToMany()を使うことが適切なようです。(参考までにですが、こちらにサンプルコードがあります)

belongsToMany()の使い方のサンプル

ここではArticle(ブログ記事)とTag(タグ)という2つのモデルについて、hasManyThrough()を使って多対多の関係を定義してコンソールで確認できるようになるまでのサンプルをまとめました。

このようなケースではarticlesとtagsとarticle_tagの3つのテーブルが必要になります。中間テーブルに関してですが、関連するモデル名をアルファベット順で並べたarticle_tagという名前になり、article_idとtag_idというカラムが必要になります。(独自のテーブル名を用いることも可能ですが、その場合はhasManyThrough()の定義の際にテーブル名などの記述も必要になります。)

マイグレーションファイルと各モデルの作成

まずはブArticle(ブログ記事)とTag(タグ)という2つのモデルを作成し、同時にマイグレーションファイルも作成(-mオプションを使う)します。

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

次に中間テーブルのマイグレーションファイルを作成します。中間テーブルに関しては独自のメソッド等必要なければモデルの定義は不要です。先述したように、テーブル名は関連するモデル名をアルファベット順で並べたarticle_tagという名前になり、article_idとtag_idというカラムが必要になります。

$ php artisan make:migration create_article_tag_table --create=article_tag

Articleのマイグレーションファイルにはbody(本文)というカラムを追加しておきます。

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

Tagのマイグレーションファイルにはname(タグの名前)というカラムを追加しておきます。

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

中間テーブルのマイグレーションファイルにはArticleとTagの外部キーをそれぞれ追加する必要があります。

    public function up()
    {
        Schema::create('article_tag', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('article_id');
            $table->integer('tag_id');
            $table->timestamps();
        });
    }

テーブルの定義が終わったので、マイグレーションを実行します。

$ php artisan migrate

多対多のリレーションを各モデルに定義する

最後にArticleとTagのモデルにそれぞれbelongsToMany()を使って多対多の定義を追加します。

App\Article.php

class Article extends Model
{
    /**
     * Articleに紐付いたTagのリスト
     */
    public function tags()
    {
        return $this->belongsToMany('App\Tag');
    }
}

App\Tag.php

class Tag extends Model
{
    /**
     * Tagに紐付いたArticleのリスト
     */
    public function articles()
    {
        return $this->belongsToMany('App\Article');
    }
}

多対多のリレーションをコンソールで確認してみる

最後にコンソール(tinker)からリレーションが正しく定義されているかどうかを確認してみます。

$ php artisan tinker

ArticleとTagのテストデータをまずは投入しておきます。ここではArticleを一つとTagを3つ投入しました。

>>> App\Article::insert(["body" => "foo"])
>>> App\Tag::insert([["name" => "B"], ["name" => "B"], ["name" => "C"]])

リレーションの紐づけ・解除

新たに紐づけするにはattach()を使う

belongsToMany()で定義したtags()から、attach()というメソッドを使って紐づけ対象のidを引数にしてリレーションを紐付けることができます。 中間テーブルにレコードが自動的に挿入されます。

>>> $a = App\Article::first() // Articleを取得
>>> $a->tags()->attach(1) // id=1のTagを紐付ける

以下のように、attach() で紐づけられたTagを取得することができました。

>>> $a = App\Article::first() // データベース上は紐づけされてもインスタンスは更新されないようなので改めて更新されたArticleを取得する
>>> $a->tags // 紐付いたタグを取得する
=> Illuminate\Database\Eloquent\Collection {#2950
     all: [
       App\Tag {#2932
         id: 1,
         name: "A",
         created_at: null,
         updated_at: null,
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#2942
           article_id: 1,
           tag_id: 1,
         },
       },
     ],
   }

紐づけを解除するにはdetach()を使う

detach()というメソッドはattach()の逆で、ヒモ付の解除をすることができます。中間テーブルのレコードが自動的に削除されます。各モデルのレコードには影響がありません。

>>> $a->tags()->detach(1) // id=1のTagの紐付けを解除する

以下のように、detach()したことで紐付けられたTagが削除されたことが確認できました。

>>> $a = App\Article::first() // 更新されたArticleを取得する
>>> $a->tags
=> Illuminate\Database\Eloquent\Collection {#2958
     all: [],
   }

まとめての紐づけと紐づけの解除を行うsync()

他にもsync()という便利なメソッドがあります。紐づけるidのリストを引数として渡すことで、まとめて紐づけを行い、かつリストにないidは全て自動的に削除することができます。

こちらも同様に中間テーブルのレコードの追加・削除が自動的に行われ、各モデルのレコードには影響がありません。

>>> $a->tags()->sync([2, 3]) // id=1とid=2のTagを紐づけ、これ以外のものは紐づけを解除する

以下のように、まとめて紐づけされたTagを取得することができました。

>>> $a = App\Article::first() // 更新されたArticleを取得する
>>> $a->tags
=> Illuminate\Database\Eloquent\Collection {#2962
     all: [
       App\Tag {#2957
         id: 2,
         name: "B",
         created_at: null,
         updated_at: null,
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#2956
           article_id: 1,
           tag_id: 2,
         },
       },
       App\Tag {#2966
         id: 3,
         name: "C",
         created_at: null,
         updated_at: null,
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#2963
           article_id: 1,
           tag_id: 3,
         },
       },
     ],
   }

おまけ:hasManyThrough()を使って紐付いたタグを取得する

本来は多対多の関係に関しては上に書いてきたようにbelongsToMany()を使って定義するべきなのですが、紐付いたタグの取得という部分に関しては、3つの関連するテーブルのリレーションということでhasManyThroughでも取得は可能なようでした。しかし、hasManyThroughは取得の際のショートカットのようなものに過ぎないようなのでこの場合は当然attach/detach/syncなどのメソッドは使うことはできません。多対多のリレーションに関しては常にbelongsToMany()で定義するべきなようです。

あくまで参考ですが、上のサンプルに以下のような修正を行うことでhasManyThrough()でもArticleに紐づくTagを取得することができます。

中間テーブルのモデルArticleTagのモデルを定義するファイルを作成し、テーブル名を定義します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class ArticleTag extends Model
{
    protected $table = 'article_tag'; // テーブル名を定義する。
}

ArticleのTagとのリレーションをhasManyThrough()で定義するように修正します。ドキュメントにあるような形のリレーションとは異なるのでローカルキーや外部キーの指定の仕方が若干トリッキーです・・。

    public function tags() {
        return $this->hasManyThrough(
            'App\Tag',
            'App\ArticleTag',
            'article_id',
            'id',
            'id',
            'tag_id'
        );
    }
>>> $a = App\Article::first() // Articleを取得する
>>> $a->tags
=> ... 紐付けられたタグが取得できる。

このように、hasManyThrough()で定義したリレーションを使って紐付けられたタグを取得することが可能です。しかし、紐づけや紐づけの削除を行うには手動で中間テーブルのレコードをの追加・削除を行う必要があるので、多対多の関係にはドキュメント通りにbelongsToMany()を使いましょう!

← 前の投稿

S3とCloudFront関連のあれこれ

次の投稿 →

Elasticsearch for Apache Hadoopを使ってSparkからAmazon ESにデータと連携してみた

コメントを残す