LoginSignup
8

posted at

Laravel 多対多のリレーション

はじめに

ECサイトで、注文完了後に確認メールを送信する機能をMailableクラスを用いて実装しました。
Viewにデータを渡すために、publicプロパティ($order)に注文時のOrderモデルのインスタンスをコンストラクタで初期化するのですが、メール本文にはOrderモデルのインスタンスに関連するモデル(Items、OrderItems)インスタンスの情報も記載したかったのでLaravelのORM機能のリレーションを使って実現する方法を紹介します。

Ordered.php
<?php

namespace App\Mail;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class Ordered extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * 注文インスタンス
     *
     * @var \App\Models\Order
     */
    public $order;

    /**
     * 新しいメッセージインスタンスの生成
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * メッセージを作成
     *
     * @return $this
     */
    public function build()
    {
        return $this->from('ordered@example.com','株式会社hogehoge')
                    ->subject('注文完了のお知らせ')
                    ->view('emails.orders.ordered');
    }
}
OrderController.php
//Mailerでメールを送信
        $order = Order::find($order->id);//注文確定後の情報を取得
        Mail::to(Auth::user())->send(new Ordered($order));
        return view('order_finished');

ER図

スクリーンショット 2022-10-04 11.21.36.png

ER図について

テーブル構成

  • ordersテーブル:注文管理用テーブル
  • itemsテーブル:商品管理用テーブル
  • orderitemsテーブル:中間テーブル

各テーブルのリレーション

  • orders-orderitems:1対多
  • items-orderitems:1対多
  • orders-items:多対多

モデルの構造

多対多の関係は、belongsToManyメソッドの結果を返すメソッドを作成して定義します。belongsToManyメソッドは、アプリケーションのすべてのEloquentモデルで使用しているIlluminate\Database\Eloquent\Model基本クラスが提供しています。

引用:Laravel 9.x Eloquent:リレーション

以下が完成系のコードになります。

Order.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Orderitem;

class Order extends Model
{
    use HasFactory;
    //この部分を追記
    public function items()
    {
        return $this->belongsToMany(Item::class, 'orderitems')->as('orderitem')->withPivot('quantity', 'gender');
    }
}

OrderからItemへのアクセス

基本系

Order.php
public function items()
    {
        return $this->belongsToMany(Item::class);
    }

基本的にこれで、items動的リレーションプロパティを使用して注文に関連する商品へアクセスできるようになります。しかし、今回の場合は、中間テーブル名がLaravelの命名規則に則っていないため、リレーションが失敗してしまいます。

リレーションの中間テーブルのテーブル名を決定するために、Eloquentは2つの関連するモデル名をアルファベット順に結合します。
ただし、この規約は自由に上書きできます。その場合、2番目の引数をbelongsToManyメソッドに渡します。
return $this->belongsToMany(Role::class, 'role_user');

今回の場合だと、中間テーブル名は'item_order'である必要があります。しかし実際の中間テーブル名は'orderitems'となっています。そのため、ただし書きにあるように第2引数で'orderitems'と名前を上書きする必要があります。

Order.php
 public function items()
    {
        return $this->belongsToMany(Item::class, 'orderitems')
    }

OrderからOrderitem(中間テーブル)へのアクセス

Userモデルに関連するRoleモデルがたくさんあるとしましょう。この関係にアクセスした後、モデルのpivot属性を使用して中間テーブルにアクセスできます。

取得する各Roleモデルには自動的にpivot属性が割り当てられることに注意してください。この属性には、中間テーブルを表すモデルが含まれています。

use App\Models\Order;

$order = Order::find(1);

foreach ($order->items as $item) {
    echo $item->pivot->id;
}
#=>3//Orderに紐づくItemの主キー

リレーションで取得したかくモデルインスタンスへ自動でpivotプロパティが割り当てられるので、このプロパティへアクセスすることで中間テーブルへアクセスすることができます。

デフォルトでは、モデルキーのみがpivotモデルに存在します。中間テーブルに追加の属性を含めている場合は、関係を定義するときにそうした属性を指定する必要があります。

しかし、デフォルトではpivotモデルは主キーの情報しか持っていません。今回は'quantity'と'gender'属性を追加で中間テーブルが持っているので、pivotモデルにそれらの属性を持たせる記述を追加します。

Order.php
public function items()
    {
        return $this->belongsToMany(Item::class)->withPivot('quantity', 'gender');
    }

上記のように記述することで、以下のように指定した属性へアクセスすることができるようになります。

use App\Models\Order;

$order = Order::find(1);

foreach ($order->items as $item) {
    echo $item->pivot->quantity;
    echo $item->pivot->gender;
}
#=>2//Orderに紐づくOrderitemの数量
#=>オス//Orderに紐づくOrderitemの性別

pivot属性名のカスタマイズ

現状、中間テーブルにはpivot属性を介してアクセスすることができますが、テーブル名ではないので、わかりにくい属性名です。
最後に、属性名をカスタマイズしてわかりやすい名前を指定します。

前述のように、中間テーブルの属性はモデルのpivot属性を介してアクセスできます。この属性の名前は、アプリケーション内での目的をより適切に反映するため、自由にカスタマイズできます。
たとえば、アプリケーションにポッドキャストを購読する可能性のあるユーザーが含まれている場合、ユーザーとポッドキャストの間には多対多の関係があるでしょう。この場合、中間テーブル属性の名前をpivotではなくsubscriptionに変更することを推奨します。リレーションを定義するときにasメソッドを使用して指定できます。

Order.php
public function items()
    {
        return $this->belongsToMany(Item::class, 'orderitems')->as('orderitem')->withPivot('quantity', 'gender');
    }

->as('orderitem')とすることで'pivot'ではなく'orderitem'でアクセスができるようになります。

use App\Models\Order;

$order = Order::find(1);

foreach ($order->items as $item) {
    echo $item->orderitem->quantity;
    echo $item->orderitem->gender;
}
#=>2//Orderに紐づくOrderitemの数量
#=>オス//Orderに紐づくOrderitemの性別

Viewの編集(メール本文)

emails/orders/ordered.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body> 
    <h1>{{ $order->destination_name }}様 ご注文ありがとうございました。</h1>
    <h2>注文内容詳細</h2>
    <div>
        @foreach ($order->items as $item)
            <img src="{{asset('img/' . $item->item_img)}}" width="193" height="130"/>
            <ul>
                <li>商品名:{{ $item->item_name }}</li>
                <li>商品説明:{{ $item->description }}</li>
                <li>性別:{{ $item->orderitem->gender }}</li>
                <li>数量:{{ $item->orderitem->quantity }}</li>
            </ul>   
        @endforeach
    </div>
    <div>
        注文日時:{{ $order->created_at }}
    </div>
    <div>
        注文合計額:{{ number_format($order->total_price) }}円
    </div>
    <h3>配送先</h3>
    <div>
        郵便番号:{{$order->destination_zipcode}}
    </div>
    <div>
        住所:{{$order->destination_address}}
    </div>
    <div>
        連絡先:{{$order->destination_tel}}
    </div>
</body>
</html>

多対多のリレーションをモデルクラスで定義したことにより、
以下のようにしてOrderモデルから関連する各モデルの情報へアクセスすることができるようになります。

<div>
        @foreach ($order->items as $item)
            <img src="{{asset('img/' . $item->item_img)}}" width="193" height="130"/>
            <ul>
                <li>商品名:{{ $item->item_name }}</li>
                <li>商品説明:{{ $item->description }}</li>
                <li>性別:{{ $item->orderitem->gender }}</li>
                <li>数量:{{ $item->orderitem->quantity }}</li>
            </ul>   
        @endforeach
    </div>

逆の関係性

Item.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    use HasFactory;

    public function orders(){
        return $this->belongsToMany(Order::class, 'orderitems')->as('orderitem')->withTimestamps()->withPivot('quantity','gender');
    }
}

これでItemモデルのインスタンスからOrderモデルのインスタンスにアクセスすることができます。

use App\Models\Item;

$item = Item::find(1);

foreach ($item->orders as $order) {
    echo $order->id;
    #=>idが1のItemモデルのインスタンスに紐づくOrderモデルインスタンスのid
}

まとめ

  • 多対多のリレーションには両方のidを外部キーとして持つ中間テーブル(pivot table)が必要。
  • モデルクラスにbelongsToMany(モデルファサード::class)記述することで多対多の関係性を表現できる。
  • 中間テーブル名は、2つの関連するモデル名をアルファベット順に結合したものがデフォルト。名前を変更するためには、belongsToManyの第2引数に中間テーブルの名前を渡す。
  • 中間テーブルを表すpivotモデルはデフォルトでid属性しか持たない。そのため、withPivot()で属性を追加する必要がある。
  • 関連するテーブルから中間テーブルへは、pivot属性を用いてアクセスすることができるがこの属性名を変更するためには、as()に指定する属性名を渡す。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
8