ピボット上のリレーションの 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);