Laravel - 通过 pivot table 与急切加载的一对一关系
Laravel - one-to-one relation through pivot table with eager load
我有这个关系
- 一个动作可以有多个步骤
- 一个Step可以属于多个Movements
所以必须创建一个主元 table 和一个 belongsToMany
关系,但是我的主元 table 有一些额外的列,比如 finished
和 order
我想要两个关系,一个从运动中获取所有步骤,另一个从运动中获取当前步骤(最后完成的步骤)
我知道如何获得所有步骤
public function steps()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->orderBy('pivot_order');
}
但是当前步骤呢?我需要这种关系,但只返回一条记录并且能够急切加载它,因为我将它传递给 vue.js
public function current_step()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->where('finished', true)
->orderBy('pivot_order', 'desc');
}
注意,我想在没有额外包的情况下这样做
替代方案,但有额外的包: (不是标记为正确的答案,来自@cbaconnier的答案)
Laravel 有一种方法可以 create getters and setters 其行为类似于数据库中的列。这些可以完美解决您的问题,您可以将它们附加到您的连载中。
所以您的 current_step 将成为访问器 (getter)。该函数的语法是 getCurrentStepAttribute()
,这将使它可以在 current_step
属性 上访问。为避免 N + 1,请在使用 with('steps')
方法检索模型时预先加载步骤。这比 运行 作为查询更好,因为它总是执行 N 次。
public function getCurrentStepAttribute() {
return $this->steps
->where('finished', true)
->sortByDesc('pivot_order')
->first();
}
现在您可以在 Movement.php class 上使用附加 属性 来包含您的 Eloquent 访问器。
protected $appends = ['current_step'];
与@mrhn 提供的答案不同的方法是创建自定义关系。来自 Spatie 的 Brent 对此做了 excellent article
尽管我的回答将执行与 staudenmeir's package 提供的完全相同的查询,但它让我意识到,无论您使用该包、这个答案还是@mrhn 答案,您都可以避免 n+1 个查询但你最终可能还是会得到大量水合模型。
在这种情况下,我认为不可能避免一种或另一种方法。缓存可能是一个答案。
由于我不完全确定您的架构,我将使用我的 .
中的用户照片示例提供我的解决方案
User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function photos()
{
return $this->belongsToMany(Photo::class);
}
public function latestPhoto()
{
return new \App\Relations\LatestPhotoRelation($this);
}
}
LastestPhotoRelation.php
<?php
namespace App\Relations;
use App\Models\User;
use App\Models\Photo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class LatestPhotoRelation extends Relation
{
/** @var Photo|Builder */
protected $query;
/** @var User */
protected $user;
public function __construct(User $user)
{
parent::__construct(Photo::query(), $user);
}
/**
* @inheritDoc
*/
public function addConstraints()
{
$this->query
->join(
'user_photo',
'user_photo.photo_id',
'=',
'photos.id'
)->latest();
// if you have an ambiguous column name error you can use
// `->latest('movement_movement_steps.created_at');`
}
/**
* @inheritDoc
*/
public function addEagerConstraints(array $users)
{
$this->query
->whereIn(
'user_photo.user_id',
collect($users)->pluck('id')
);
}
/**
* @inheritDoc
*/
public function initRelation(array $users, $relation)
{
foreach ($users as $user) {
$user->setRelation(
$relation,
null
);
}
return $users;
}
/**
* @inheritDoc
*/
public function match(array $users, Collection $photos, $relation)
{
if ($photos->isEmpty()) {
return $users;
}
foreach ($users as $user) {
$user->setRelation(
$relation,
$photos->filter(function (Photo $photo) use ($user) {
return $photo->user_id === $user->id; // `user_id` came with the `join` on `user_photo`
})->first() // Photos are already DESC ordered from the query
);
}
return $users;
}
/**
* @inheritDoc
*/
public function getResults()
{
return $this->query->get();
}
}
用法
$users = \App\Models\User::with('latestPhoto')->limit(5)->get();
与 Brent 文章的主要区别在于,我们不使用 Collection
,而是返回最新的照片 Model
。
我有这个关系
- 一个动作可以有多个步骤
- 一个Step可以属于多个Movements
所以必须创建一个主元 table 和一个 belongsToMany
关系,但是我的主元 table 有一些额外的列,比如 finished
和 order
我想要两个关系,一个从运动中获取所有步骤,另一个从运动中获取当前步骤(最后完成的步骤)
我知道如何获得所有步骤
public function steps()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->orderBy('pivot_order');
}
但是当前步骤呢?我需要这种关系,但只返回一条记录并且能够急切加载它,因为我将它传递给 vue.js
public function current_step()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->where('finished', true)
->orderBy('pivot_order', 'desc');
}
注意,我想在没有额外包的情况下这样做
替代方案,但有额外的包:
Laravel 有一种方法可以 create getters and setters 其行为类似于数据库中的列。这些可以完美解决您的问题,您可以将它们附加到您的连载中。
所以您的 current_step 将成为访问器 (getter)。该函数的语法是 getCurrentStepAttribute()
,这将使它可以在 current_step
属性 上访问。为避免 N + 1,请在使用 with('steps')
方法检索模型时预先加载步骤。这比 运行 作为查询更好,因为它总是执行 N 次。
public function getCurrentStepAttribute() {
return $this->steps
->where('finished', true)
->sortByDesc('pivot_order')
->first();
}
现在您可以在 Movement.php class 上使用附加 属性 来包含您的 Eloquent 访问器。
protected $appends = ['current_step'];
与@mrhn 提供的答案不同的方法是创建自定义关系。来自 Spatie 的 Brent 对此做了 excellent article
尽管我的回答将执行与 staudenmeir's package 提供的完全相同的查询,但它让我意识到,无论您使用该包、这个答案还是@mrhn 答案,您都可以避免 n+1 个查询但你最终可能还是会得到大量水合模型。
在这种情况下,我认为不可能避免一种或另一种方法。缓存可能是一个答案。
由于我不完全确定您的架构,我将使用我的
User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function photos()
{
return $this->belongsToMany(Photo::class);
}
public function latestPhoto()
{
return new \App\Relations\LatestPhotoRelation($this);
}
}
LastestPhotoRelation.php
<?php
namespace App\Relations;
use App\Models\User;
use App\Models\Photo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class LatestPhotoRelation extends Relation
{
/** @var Photo|Builder */
protected $query;
/** @var User */
protected $user;
public function __construct(User $user)
{
parent::__construct(Photo::query(), $user);
}
/**
* @inheritDoc
*/
public function addConstraints()
{
$this->query
->join(
'user_photo',
'user_photo.photo_id',
'=',
'photos.id'
)->latest();
// if you have an ambiguous column name error you can use
// `->latest('movement_movement_steps.created_at');`
}
/**
* @inheritDoc
*/
public function addEagerConstraints(array $users)
{
$this->query
->whereIn(
'user_photo.user_id',
collect($users)->pluck('id')
);
}
/**
* @inheritDoc
*/
public function initRelation(array $users, $relation)
{
foreach ($users as $user) {
$user->setRelation(
$relation,
null
);
}
return $users;
}
/**
* @inheritDoc
*/
public function match(array $users, Collection $photos, $relation)
{
if ($photos->isEmpty()) {
return $users;
}
foreach ($users as $user) {
$user->setRelation(
$relation,
$photos->filter(function (Photo $photo) use ($user) {
return $photo->user_id === $user->id; // `user_id` came with the `join` on `user_photo`
})->first() // Photos are already DESC ordered from the query
);
}
return $users;
}
/**
* @inheritDoc
*/
public function getResults()
{
return $this->query->get();
}
}
用法
$users = \App\Models\User::with('latestPhoto')->limit(5)->get();
与 Brent 文章的主要区别在于,我们不使用 Collection
,而是返回最新的照片 Model
。