Edited at

【Laravel】 Eager Loading 応用: ピボット上のリレーションの Eager Loading


ピボット上のリレーションの Eager Loading


定義


app/Concerns/Relations/EagerBelongsToMany.php

<?php

declare(strict_types=1);

namespace App\Concerns\Relations;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Collection;

/**
* Class EagerBelongsToMany
*
* BelongsToMany と同一ですが,ピボット上のリレーションのEagerローディングに対応しています。
*/

class EagerBelongsToMany extends BelongsToMany
{
/**
* ロードしたいピボット上のリレーションを定義します。
*
* @var array
*/

protected $pivotRelations = [];

/**
* 所有者側からみたピボットリレーションを生成しつつ,ピボット上のリレーションもロードします。
*
* @param Model[] $models
*/

protected function hydratePivotRelation(array $models): void
{
parent::hydratePivotRelation($models);

(new Collection($models))->map->{$this->accessor}->load($this->pivotRelations);
}

/**
* ロードしたいピボット上のリレーションを定義します。
*
* @param $relations
* @return $this
*/

public function withPivotRelation($relations): self
{
$this->pivotRelations = array_merge(
$this->pivotRelations, is_array($relations) ? $relations : func_get_args()
);

return $this;
}

/**
* 除外したいピボット上のリレーションを定義します。
*
* @param $relations
* @return $this
*/

public function withoutPivotRelation($relations): self
{
$this->pivotRelations = array_values(array_diff(
$this->pivotRelations, is_array($relations) ? $relations : func_get_args()
));

return $this;
}
}



app/Concerns/HasExtendedRelationShips.php

<?php

declare(strict_types=1);

namespace App\Concerns;

use App\Concerns\Relations\EagerBelongsToMany;
use Illuminate\Database\Eloquent\Model;

/**
* Trait HasExtendedRelationships
*
* 拡張リレーションを定義します。
*
* @mixin Model
*/

trait HasExtendedRelationships
{
/**
* belongsToMany と同一ですが,ピボット上のリレーションのEagerローディングに対応しています。
* コンストラクタコール部分以外はコピペです。
*
* @param string $related
* @param string $table
* @param string $foreignPivotKey
* @param string $relatedPivotKey
* @param string $parentKey
* @param string $relatedKey
* @param string $relation
* @return EagerBelongsToMany
*/

public function eagerBelongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null,
$parentKey = null, $relatedKey = null, $relation = null): EagerBelongsToMany
{
// If no relationship name was passed, we will pull backtraces to get the
// name of the calling function. We will use that function name as the
// title of this relation since that is a great convention to apply.
if ($relation === null) {
$relation = $this->guessBelongsToManyRelation();
}

// First, we'll need to determine the foreign key and "other key" for the
// relationship. Once we have determined the keys we'll make the query
// instances as well as the relationship instances we need for this.
$instance = $this->newRelatedInstance($related);

$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();

$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();

// If no table name was provided, we can guess it by concatenating the two
// models using underscores in alphabetical order. The two model names
// are transformed to snake case from their default CamelCase also.
if ($table === null) {
$table = $this->joiningTable($related);
}

return new EagerBelongsToMany(
$instance->newQuery(), $this, $table, $foreignPivotKey,
$relatedPivotKey, $parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(), $relation
);
}
}



利用例


app/Community.php

// コミュニティ

class Community extends Model {}


app/User.php

// ユーザ

class User extends Model
{
use HasExtendedRelationShips;

public function communities(): Builder
{
return $this->eagerBelongsToMany(Community::class)
->as('membership')
->using(Membership::class)// ピボットクラスの指定
->withPivot('registered_at')// ピボット上の読み込むカラムの指定
->withPivotRelation('payment');// ピボット上の読み込むリレーションの指定
}
}



app/Membership.php

// ユーザは入会情報テーブルを通じてコミュニティに属している

// 更に入会情報テーブルから支払い情報にもリンクされている
class Membership extends Pivot
{
protected $table = 'memberships';

public function payment(): Builder
{
return $this->belongsTo(Payment::class);
}
}



app/Payment.php

// 支払い情報

class Payment extends Model {}

$users = User::with('communities')->whereIn('id', [1, 2, 3])->get();

// これらの User が所持しているすべての Payment を取得
// Eager Loading により N+1 問題回避可能
dd($users->map->communities->map->membership->map->payment);