【Laravel】リレーションを理解しよう!

対象 LaravelでhasManyとかbelongsToとか分からない人

書いている人 プログラミング歴2年

前提 Laravelプロジェクトを作成できる 主テーブル従テーブル、外部キーといった概念が分かる。

環境 Mac:M1 Laravelバージョン:9

参考 https://readouble.com/laravel/9.x/ja/eloquent-relationships.html

本来は外部キーとリレーションの記事をまとめるはずだったのだが、外部キーが結構長くなってしまったので別記事としてリレーションについてまとめます。外部キーに関してよくわからない人は下の記事を参考にしてみてください

目次

リレーションとは

リレーションは正確にはリレーションシップといい、テーブル同士の関係性のことだ。なにもLaravelの専門用語というわけではなく、RDBMS(MySQLや、MariaDB等)のDBを扱う時の言葉だ。
どのようにテーブル同士を関係づけているかというと、外部キーと呼ばれるカラムを片方のテーブルに持たせることで、テーブルとテーブルを関係づけている。

テーブル同士の関係には以下の3つがある

  • 1 対 1・・・外部キーの各項目が一つのレコードにしか現れないよう制約する
  • 1 対 多 ・・・外部キーの各項目が複数のレコードに繰り返し登場する
  • 多 対 多 ・・・両テーブルのフィールドが互いに相手方へ(一対多の)リレーションシップを張った状態

定義は以下のリンクから引用しています。

一般的なリレーションシップでは外部キーの各項目が複数のレコードに繰り返し登場することがあり、このような関係を「一対多リレーションシップ」という。外部キーの各項目が一つのレコードにしか現れないよう制約する「一対一リレーションシップ」もあるが、使われる場面は少ない。また、両テーブルのフィールドが互いに相手方へ(一対多の)リレーションシップを張った状態を「多対多リレーションシップ」という場合がある。

https://e-words.jp/w/%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%B7%E3%83%83%E3%83%97.html

文章で書くより図の方がわかりやすいと思うので図にしてみる。

1 対 1 

idname
1イチロー
2ジロー
usersテーブル
idnameuser_id
1ラーメン二郎2
2イチローラーメン1
ramen_shopsテーブル

まずは1対1のテーブル関係から。

ここでは、ラーメン店主とラーメン店舗の関係をusersテーブルとramen_shopsテーブルで表してみた。店主は自分の店舗を経営し、店舗からは、誰が自分を運営しているのが分かる。(店主が2店舗以上経営している可能性は一旦無視してください。)

このように、ramen_shopsテーブルにある外部キーuser_idの値に同じ値が表示されないような関係が1 対 1だ。

1 対 多

idname
1ドラえもん
2のび太
usersテーブル
idtweetuser_id
1宿題やりたくないな〜2
2サボって遊びにいこうっと。2
3どら焼き食べたい1
tweetsテーブル

次は1対多。

ここではツイッターのようなテーブル関係を見てみる。

usesテーブルとtweetsテーブルの関係は1 対 多になる。なぜなら、ユーザーはたくさんのツイートを持つことができるのに対して、ツイートは1人のユーザーにしか属さないからだ。

この場合、usersテーブルが1でTweetsテーブルが多になる。

1対1と違って、tweetsテーブルの外部キーuser_idには同じ値が登場する。1対多の関係はよく出てくるので覚えておこう。

多 対 多

idname
1イチロー
2ジロー
3サブロー
usersテーブル
iduser_idcomic_id
111
213
321
432
533
comic_userテーブル(中間テーブル)
idtitle
1ドラえもん
2鉄腕アトム
3ワンピース
comicsテーブル

最後に多対多。どちらのテーブルももう一方のテーブルに対して、1対多の関係になるややこしいリレーションだ。

この多対多では、整合性を持たせるために、中間テーブルという3つ目のテーブルを用意する必要がある。一番ややこしい。

ここでは、ユーザーと持っている漫画の関係をテーブルで表してみる。ユーザーは複数の漫画を所有しているし、漫画は複数のユーザーから所持される。

これを2つのテーブルだけでやろうとすると、1つのカラムに複数の値が入ることになる。それはアンチパターンといってよくない設計になってしまう。

ここで、お互いの外部キーを中間テーブルに持たせるとわかりやすくなる。
例えば、上の図でユーザー1のイチローさんは、ドラえもんとワンピースを所持しており、ドラえもんはイチローさんとジローさんに所有されている。このようにお互いの外部キーに同じ値を登場させることができるのが中間テーブルだ。

Laravelでは中間テーブル名を命名する際、「お互いのテーブル名を単数系でくっつける。(アルファベット順)」という決まりがある。ここではそれに沿って、「comic_userテーブル」と名付けている。

多対多に関して、以下の記事が鬼のようにわかりやすかったのでぜひ参考にしてみてください。

やさしい図解で学ぶ 中間テーブル 多対多 概念編
https://qiita.com/ramuneru/items/db43589551dd0c00fef9

以上のようなリレーションシップを頭に入れながら、Laravelでそれをどうやって扱うのかを次に見ていこう。

Laravelのリレーション


ここまでリレーションについてはなんとなく理解いただけただろうか?
それを踏まえて、Laravelのリレーションとは一体なんなのだろうか?

リレーションという言葉には上で説明したようなテーブルとテーブルの関係といった意味合いが含まれているが、Laravelでリレーションを指す場合は、Eloquent:リレーションのことを指す。

Eloquentリレーションとは公式から引用すると以下になる。

公式から。
Eloquentの項目の一部としてRelationshipsが定義されている。

上のようにEloquentリレーションはメソッドとして定義することができる。

Modelクラスに以下のような形でメソッドを定義して、それからコントローラー等で定義したメソッドを使っていくイメージだ。

commentsメソッドをモデルで定義。このメソッドがLaravelのリレーションメソッドだ。

Eloquentリレーションをメソッドとして定義すると、上のような形のメソッドを使用して関連するデータを引っ張ってくることができる。リレーションの何が便利かっていうと、簡単に紐づいているレコードを取得することができるようになるということだ。

メソッドの中で$this->hasMany()としている部分がテーブル同士の関係によって変わってくるメソッドだ。ここでは、hasManyメソッドを使用しているので1対多の関係であることがわかる。ここの書き方を次から見ていく。

また、定義したリレーションメソッドはプロパティとしても使えるし、クエリビルダとしても使うことができる。

Postテーブルid1に紐づくコメントの一覧をcommentsメソッドで取得できる。

主テーブルと従テーブル
リレーションを扱う上でこの言葉が何度か登場するので先に紹介しておく。
主テーブルと従テーブルは、テーブル同士の関係の概念のこと。
外部キーが設定されている方が従。されていなければ主と理解すれば基本的には問題ないと思う。
この先何度も登場するのでしっかり押さえておこう。

1 対 1

まずは1対1のリレーションから、ここで登場するのがhasOneメソッドとbelongsToメソッドだ。テーブルのリレーションは上で説明したものを使用する。各テーブルやモデルは既に作成済みで、データも挿入済み前提で話を進めていく。

idname
1イチロー
2ジロー
usersテーブル
idnameuser_id
1ラーメン二郎2
2イチローラーメン1
ramen_shopsテーブル

hasOne

まずはhasOneメソッドから。ユーザーは店舗(ラーメン屋)を1つ持っており、店舗はユーザーに所属している。主従関係で言えば、ユーザーが主で店舗が従だ。このメソッドは以下のような形で定義する。

public function ramenShop()
    {
        return $this->hasOne(RamenShop::class);
    }

まずリレーションメソッドを書く場所はModelクラス。
どちらのモデルクラスにもリレーションメソッドを書くことはできるが、hasOneメソッドは主テーブルのリレーションメソッドの中で定義する。
今回の場合はusersテーブルが主テーブルとなので、Userモデルクラスにメソッドを定義しよう。(元々、Userモデルには色々と定義されているので一番下の行にメソッドを追加する)

リレーションメソッド名は基本的にもう片方のモデル名をつける。この場合はusersテーブルからramen_shopsテーブルなので、ramenShopメソッドと名前をつける。
そのメソッドの中でhasOneメソッドを使い、引数に対象のモデルクラスを入れる。

これで定義は完了だ。これをさっそく使ってみよう。

UserControllerファイルを開いて以下のように定義する。

 public function index()
    {
        $ramenShop = User::find(1)->ramenShop;
        dd($ramenShop);
    }

3行目でusersテーブルからid1のユーザーを抜きだし、上で定義付けたリレーションメソッドのramenShopメソッドを呼び出している。ddで中身を確認するとuser_id1を持つramenShopレコードの値が取得できていることがわかる。

attributesの中にデータが入っている。

レコードが取れたら、後は$ramenShop->カラム名で値を取得することができる。

use App\Models\User;
 
public function index()
    {
        $ramenShop = User::find(1)->ramenShop;
        dd($ramenShop->name);
    }
name名がとれた。

$ramenShopごとbladeに渡してあげれば、$ramenShop->カラム名で展開できるので便利だ。

注意点として、モデルクラスを使うためにuseで名前空間を指定することを忘れないようにしよう。

belongsTo

次はbelongToメソッドだ。

hasOneメソッドを使って、ユーザーから店舗(ラーメン屋)の値を取得することができたが、今度は店舗からユーザーの情報を取得したい。そこで使用するのが、belongsToメソッドだ。belongsToは従テーブルのリレーションメソッドの中に記載する。
名前から分かるように「何かに属する」と言った意味がbelongsToにはあるので、「従う」という漢字と関連づけて従テーブルで使うことを覚えよう。

従テーブルのModelクラスである、RamenShopモデルにuserメソッドを定義。こちらも、もう片方のモデル名をつけ、その中でbelongsToメソッドを定義して、引数に対象のモデルクラスを入れる。

public function user()
    {
        return $this->belongsTo(User::class);
    }

リレーションメソッドを定義したら、さっそく使ってみる。RamenShopControllerで以下のように定義して実行してみる。

use App\Models\RamenShop;

public function index()
    {
        $user =RamenShop::find(1)->user;
        dd($user);
    }

ramen_shopsテーブルのid1のレコードに記載されているuser_idは2なので、usersテーブルからid2のジローさんが取得できた。後は、user->カラム名として展開して使用することができるぞ。

user->nameとすればname属性の値が取得できる。

注意点

ここまで、特に説明していなかったが、そもそもLaravelでリレーションをするなら外部キーをあらかじめ設定する必要がある。つまり、user_idという外部キーがなければそもそもリレーションをすることができないので注意しておこう。

ただし、外部キーを以下のようにintegerとしてもリレーションを使うことができる。user_idというカラムさえ用意しておけば使えるということだ。ただ、制約をかけておかないと、user_idカラムに同じ値が入ってしまうなどの矛盾が生じることもあるので基本的には制約をかけるのがよいと思う。(ここでは1対1の関係なのでuser_idに同じ値は入らないようにしないといけない。)

そこら辺は関わるプロジェクトによっても違うかもしれないので、頭の片隅にでも入れておくとよいかも。

ただのintegerとしてuser_idを定義してもLaravelのリレーションは使える。

1 対 多

次は1対多のリレーション。ここで登場するのはhasManyメソッドとbelongsToメソッドだ。belongsToは上で説明したやり方と一緒だ。テーブルは以下を使用する。

idname
1ドラえもん
2のび太
usersテーブル
idtweetuser_id
1宿題やりたくないな〜2
2サボって遊びにいこうっと2
3どら焼き食べたい1
tweetsテーブル

hasMany

まずはhasManyメソッドから。これは主テーブル側(この場合はusersテーブル)に以下のように定義する。

public function tweets()
    {
        return $this->hasMany(Tweet::class);
    }

ポイントはhasOneメソッドと違って、メソッド名がtweetsと複数になるところだ。その名の通り、hasManyは外部キーを持つtweetを複数取得する。なので、ここでは複数形で記載する。その中でhasManyメソッドを定義して、対象のModelクラスを引数に入れてあげる。

定義したらさっそく使っていく。

 public function index()
    {
        $tweets = User::find(2)->tweets;
        dd($tweets);
    }

usersテーブルのid2ののび太のtweetを取得すると、複数のTweetモデルを囲んだEloquent\Collectionクラスが返ってくる。

複数のTweetモデルが取得できている。
attributesの中に値が入っている

Collectionクラスは配列をちょっと強化したもので、ほぼ配列と思って問題ない。これをBladeに渡して使ってみる。

 public function index()
    {
        $tweets = User::find(2)->tweets;
        return view('test', ['tweets' => $tweets]);
    }
@foreach ($tweets as $tweet)
<p>{{ $tweet->id }}:{{ $tweet->tweet }}</p>
@endforeach
bladeの中でforeachで回して表示

上で見る通り、$tweetsをforeachでグルグル回してそこから各カラムを表示できる。
基本的に配列やコレクションはforeachでグルグル回して使うことを覚えておこう。

belongsTo

次はbelongsTo。これは、hasOneで紹介したものと一緒で従テーブルから呼ぶ。やり方は変わらないので、コードだけさっと貼っておく。

 public function user()
    {
        return $this->belongsTo(User::class);
    }
use App\Models\Tweet;

public function index()
    {
        $user = Tweet::find(1)->user;
        dd($user);
    }
Tweetsテーブルのid1に紐づくuserが取得できた。

多 対 多

次は多対多。上でも述べたように、多対多の場合は中間テーブルを作成する必要がある

idname
1イチロー
2ジロー
3サブロー
usersテーブル
iduser_idcomic_id
111
213
321
432
533
comic_userテーブル(中間テーブル)
idtitle
1ドラえもん
2鉄腕アトム
3ワンピース
comicsテーブル

中間テーブルを以下のコマンドで作成する。

php artisan make:migration comic_user_table

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        //
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        //
    }
};

コマンドを入力すると、migationファイルができると思うので、up関数とdrop関数の中で定義していこう。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comic_user', function (Blueprint $table) {
            $table->id();
            $table->foreignId('comic_id')->constrained();
            $table->foreignId('user_id')->constrained();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('comic_user');
    }
};

ここで注意しておくのは、16行目のテーブル名を間違えないこと。上にも書いたが、Laravelでは中間テーブルは単数系のテーブル名をアルファベット順でつなぐのでここを間違えると後々エラーになる。

後は、usersテーブル、comicsテーブルどちらの外部キーも持つので設定しておく。

中間テーブルを用意して、各テーブルの構造を定義したら、シーディングも準備して、php artisan migrate:fresh --seedでテーブルの構造を反映させておこう。

次はモデルにリレーションをかけていく。

中間テーブルのモデルは?

中間テーブルにもモデルを作ってそこにメソッドなどを作成できるが、1対1や1対多に比べるとややこしいので詳しく知りたい人は公式を見てみてください。
https://laravel.com/docs/9.x/eloquent-relationships#defining-custom-intermediate-table-models

Userモデルのファイルを開いて、以下のように記述する。

    public function comics()
    {
        return $this->belongsToMany(Comic::class);
    }

ここでは、ユーザーは漫画を複数持っているので、comicsと複数系でリレーションメソッドを書いて、その中でbelongsToMany()メソッドを定義して、引数に対象のモデルクラスを入れてあげよう。

定義したらさっそく使っていこう。

    public function index()
    {
       $comics = User::find(1)->comics;
       dd($comics);
    }

上で、usersテーブルのid1のイチローさんが持つ漫画を取得している。外部キーの通りcomicのidが1と3なのでドラえもんとワンピースが取れればオッケーだ。

目的のものが取れた。

後は、$comicsをforeachで展開していって目的の値を使っていこう。

次は、逆に漫画側からユーザーを取得していく。こちらも実は上と全く一緒でbelongsToManyメソッドで同じことができる。

    public function users()
    {
        return $this->belongsToMany(User::class);
    }

Comicモデルにusersメソッドを設定して、その中でbelongsToManyメソッドを定義する。

    public function index()
    {
        $users = Comic::find(3)->users;
        dd($users);
    }

ComicControllerで定義したusersメソッドを使って、comicsテーブルのid3のワンピースを所有しているイチローさんとサブローさんが取得できればOKだ。

正しい値が取得できている。

多対多のリレーションでは、中間テーブルを噛ませることが必要になるが、belongsToManyメソッドはお互いに共通しているところはわかりやすい。

番外編:外部キーや紐づけているカラムが違う場合

Laravelは、自動的に「親モデル名_id」で外部キーを探しにいく。
ここまでは全て、「親モデル名_id」かつ、外部キーはテーブルのidカラムに紐づくものとして説明していたが、もちろん外部キーの名前を違う名前にしていたり、idカラム以外のカラムに紐づくように設定している場合もあると思う。

ここでは、hasOne、belongsTo、hasMany、belongsToManyのそれぞれのメソッドで上のような要求をどのように設定していくかを見ていく。

なお、あらかじめMigrationファイルにおいて外部キーと紐づくカラムを設定してあることが前提になる。間違っても、上記のメソッドの中で外部キーを設定できるわけではないので注意しよう。

hasOne、hasMany、belongsTo

hasOne、hasMany、belongsTo全てに共通しているのは、第二引数に外部キー、第三引数に外部キーを紐付けいているカラムを書くということだ。

hasOne


hasOneメソッドの第二引数に外部キーを指定してあげると上書きされる。何も記述しなければ、親モデル名_idを探しに行く。

public function ramenShop()
    {
        return $this->hasOne(RamenShop::class, '外部キーの名前');
    }

idカラム以外に紐づくように外部キーを設定してるなら、第三引数にそのカラム名を書いてあげる必要がある。(もちろん、migration時に設定しておく必要がある。)何も記述しなければ、親モデル名_id親モデルのidカラムを見に行く。

もし、idカラムではなく違うキーを見にいくように設定しているなら、第三引数にそのカラム名を入れてあげよう。

public function ramenShop()
    {
        return $this->hasOne(RamenShop::class, '外部キーの名前', '外部キーを紐付けいているカラム');
    }

hasMany

hasManyもhasOneと同様で第二引数と第三引数にそれぞれ、設定してあげればOKだ!

public function tweets()
    {
        return $this->hasMany(Tweet::class, '外部キーの名前', '外部キーを紐付けいているカラム');
    }

belongsTo

belongsToも上と同様で第二引数と第三引数にそれぞれ、設定してあげればOKだ!

public function user()
    {
        return $this->belongsTo(User::class, '外部キーの名前', '外部キーを紐付けいているカラム');
    }

外部キーをどちらのテーブルが持っているか把握しておこう
hasOneとhasManyにおいて「外部キー」は関連づけられるテーブルが持っており、「外部キーを紐付けいているカラム」は自身が持っている。
belongsToにおいて、「外部キー」は自身が持ち、「外部キーを紐付けいているカラム」は関連づけられるテーブルが持っている。

belongsToMany

belongsToManyの場合は、第二引数は中間テーブル名を上書きするので、第三引数と第四引数に上書きしたい外部キーを設定する。

    public function comics()
    {
        return $this->belongsToMany(Comic::class, 中間テーブル名, 外部キー名, 関連付けるモデルの外部キー名);
    }

全てに共通して言えることは、上書きしているということだ。

何度も言うように、Laravelは基本的に、Laravelの規則に則っている限り(外部キーはモデル名_idで名付ける等)自動でやってくれる。だけど、自分で外部キーを変更したりすると、規則から外れてしまうので、引数で上書きしたりしてあげる必要があるということだ。

まとめ

  • リレーションとは外部キーを持たせることで、テーブル同士を関連づけること、1対1や1対多、多対多などの関係がある。
  • LaravelではhasOne、hasMany、belongsTo、belongsToManyメソッドなどを使うと、関連づけられるテーブルのデータを簡単に取得できる。
  • 外部キーや、外部キーに紐づくカラムがLaravelの規則に則っていない場合は、各メソッドの引数で指定して上書きする。

基本的なリレーションはこれで大体まとまったのではないのだろうか?

Laravelで、リレーションは頻繁に出てくるのでしっかり押さえて自由に使えるようにしておこう。
次はhasOneThroughとhasManyThroughをまとめていきたい。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次