LoginSignup
0
0

More than 1 year has passed since last update.

[Laravel] 外部キーカラムに複数の主キーを持つ時のリレーション

Last updated at Posted at 2021-10-13

はじめに

こんな感じのテーブル構成のとき

aggregated_lists

id category_id amount_total slip_ids
1 1 400 1,2
2 2 750 3,4,5

slips

id category_id product amount
1 1 バナナ 100
2 1 りんご 300
3 2 にんじん 350
4 2 きゅうり 150
5 2 なす 250

aggregated_listsとそれに紐づくslipsのデータを以下のようにwit()やload()で簡単に取得できるようなリレーションメソッドを作ってみました。

$aggregated_list = AggregatedList::with('slips')->get();

実際のコード

リレーション名はListBelpngsToにしました。個人的にはあまりしっくりきていないのでこっちの方が良いよってのがあったらコメント頂けると嬉しいです。

元々Laravelで提供されているBelongsToを継承して実装しています。
中身のコード自体もBelongsToにかなり近いです。

ListBelongsTo.php

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ListBelongsTo extends BelongsTo
{
    /**
     * Set the base constraints on the relation query.
     *
     * @return void
     */
    public function addConstraints()
    {
        if (static::$constraints) {
            $whereIn = $this->whereInMethod($this->related, $this->ownerKey);

            $values = explode(',', $this->child->{$this->foreignKey});

            $this->query->{$whereIn}($this->ownerKey, $values);
        }
    }

    /**
     * Set the constraints for an eager load of the relation.
     * 
     * @param  array  $models
     * @return void
     */
    public function addEagerConstraints(array $models)
    {
        $key = $this->related->getTable().'.'.$this->ownerKey;

        $whereIn = $this->whereInMethod($this->related, $this->ownerKey);

        $this->query->{$whereIn}($key, $this->getEagerModelKeys($models));
    }

    /**
     * Gather the keys from an array of related models.
     *
     * @param  array  $models
     * @return array
     */
    protected function getEagerModelKeys(array $models)
    {
        $keys = [];

        foreach ($models as $model) {
            if (!is_null($value = $model->{$this->foreignKey})) {
                $values = explode(',', $value);
                $keys = array_merge($keys, $values);
            }
        }

        sort($keys);

        return array_values(array_unique($keys));
    }

    /**
     * Initialize the relation on a set of models.
     *
     * @param  array  $models
     * @param  string  $relation
     * @return array
     */
    public function initRelation(array $models, $relation)
    {
        foreach ($models as $model) {
            $model->setRelation($relation, []);
        }

        return $models;
    }

    /**
     * Match the eagerly loaded results to their parents.
     *
     * @param  array  $models
     * @param  \Illuminate\Database\Eloquent\Collection  $results
     * @param  string  $relation
     * @return array
     */
    public function match(array $models, Collection $results, $relation)
    {
        $dictionary = [];
        foreach ($results as $result) {
            $dictionary[$result->getAttribute($this->ownerKey)] = $result;
        }

        foreach ($models as $model) {
            $keys = explode(',', $model->{$this->foreignKey});

            $values = [];
            foreach ($keys as $key) {
                if (isset($dictionary[$key])) {
                    $values[] = $dictionary[$key];
                }
            }

            $model->setRelation($relation, new Collection($values));
        }

        return $models;
    }

    /**
     * Get the results of the relationship.
     *
     * @return mixed
     */
    public function getResults()
    {
        return $this->query->get();
    }
}

このままでも使えないことはないのですが、Model側で他のリレーションメソッドと同じように呼び出したいので、呼び出し用のトレイトを作成してModel側でuseします。

これもLaravel内で使われているHasRelationshipsトレイトを大胆に パクッって 参考にしています。

HasOwnRelationships.php
<?php

use App\Relations\ListBelongsTo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

trait HasOwnRelationships
{
    public function listBelongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
    {
        if (is_null($relation)) {
            $relation = $this->guessBelongsToRelation();
        }

        $instance = $this->newRelatedInstance($related);

        if (is_null($foreignKey)) {
            $foreignKey = Str::singular(Str::snake($relation)).'_'.Str::plural($instance->getKeyName());
        }

        $ownerKey = $ownerKey ?: $instance->getKeyName();

        return $this->newListBelongsTo(
            $instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
        );
    }

    protected function newListBelongsTo(Builder $query, Model $child, $foreignKey, $ownerKey, $relation)
    {
        return new ListBelongsTo($query, $child, $foreignKey, $ownerKey, $relation);
    }
}

Model側でトレイトをuseしてリレーションを定義します。

AggregatedList.php
class AggregatedList extends BaseModel
{
  use HasOwnRelationships;

  public function slips()
  {
    return $this->listBelongsTo(Slip::class);
  }
}

ここまでやるとこのように標準のリレーションと同じようにEagerローディングできます。

$aggregated_lists = AggregatedList::with('slips')->get();

$aggregated_lists = AggregatedList::get();
$aggregated_lists->load('slips');

もちろんLazyローディングも

$aggregated_list = AggregatedList::first();
$aggregated_list->slips;

最後に

もっと良くできるよーってのがありましたらぜひコメントで指摘していただけますと嬉しいです :)

0
0
0

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
  3. You can use dark theme
What you can do with signing up
0
0