Laravel belongsToMany 关系只有一个结果
Posted
技术标签:
【中文标题】Laravel belongsToMany 关系只有一个结果【英文标题】:Laravel belongsToMany relationship with only one result 【发布时间】:2019-08-30 16:10:20 【问题描述】:我在两个表之间建立了一个关系,其中一个连接表只有一个结果。
当我定义 Laravel 的 belongsToMany 关系时,我希望它单独返回该项目,而不是返回一个只有一个元素的集合。
有没有办法在 Laravel 中对此进行建模?
提前致谢。
[编辑]
我将尝试使用经典的用户/角色示例来解释我想要什么。除了users
和roles
表之外,我们还有一个users_roles
数据透视表,它将存储用户拥有的所有角色。在任何给定时间,用户只能拥有一个活动角色(由active
属性标识为true
)。
class User
function role()
return $this->belongsToMany('App\Role')->wherePivot('active', 'true');
有了这个关系定义,当我访问$user->role
时,我得到一个角色集合(只有一个元素)。我想要的是直接拥有那个 Role 实例。
【问题讨论】:
向我们展示一些您目前所获得的代码 请显示表格数据和模型 Laravel 代码 您的数据透视表是否有其他属性(除了外键)? 【参考方案1】:如果你只需要一个关系,我不知道为什么你有 belongsToMany,但是下面的代码会帮助你:
public function products()
return $this->belongsToMany('App\Product');
public function specific_product()
return $this->products()
->where('column','value')->first();
或
public function getSpecificProductAttribute()
return $this->products()
->where('column','value')->first();
【讨论】:
我在数据透视表中有几个条目,但只有一个是有效的,这就是我想要返回的那个。我喜欢你的方法。 这个解决方案的“问题”是我不能像普通的关系一样将它作为一个属性来访问...... 然后将 specific_product 改为 getSpecificProduct 即可直接从父模型中获取【参考方案2】:就我而言,这是最直接的解决方案:
class User extends Model
public function services()
return $this->belongsToMany(Service::class, 'service_user')
->using(ServiceUser::class)
->withPivot('user_id', 'service_id', 'is_main_service');
public function mainService()
return $this->hasOneThrough(Service::class, ServiceUser::class, 'user_id', 'id', 'id', 'service_id')
->where('is_main_service', 1);
数据透视表类:
use Illuminate\Database\Eloquent\Relations\Pivot;
class ServiceUser extends Pivot
【讨论】:
【参考方案3】:我遇到了这个问题,并且找到了一种非常干净的方法来解决它。
首先,更改返回belongsToMany
结果的访问器函数的名称,以反映这些返回多个结果的事实。在您的情况下,这意味着使用roles
而不是role
:
function roles()
return $this->belongsToMany('App\Role')->wherePivot('active', 'true');
然后将以下内容添加到您的模型中:
protected $appends = ['role'];
public function getRoleAttribute()
return $this->roles()->first();
现在,当您致电 $user->role
时,您将获得第一项。
【讨论】:
此解决方案在处理多条用户记录时会导致性能变差(循环查询)。 @ThanhDao - 好的 - 没有意识到这一点。您提出的解决方案是什么?可以发一下吗?【参考方案4】:我遇到了完全相同的问题,让我告诉你我是如何解决的。
在我的例子中,materials 和 customer_types 之间存在belongsToMany 关系,数据透视表包含特定客户类型的材料价格,因此数据透视表中的记录(价格)与customer_types 一样多。
我的预期:当请求特定 customer_type 的价格时,我希望获得该特定 customer_type 的范围价格作为嵌套元素。
我得到了什么:只有 1 个元素的集合。
这是我一开始在我的模型中所拥有的:
class Material extends Model
public function customer_types()
return $this->belongsToMany('App\CustomerType', 'customertype_material', 'material_id', 'customertype_id')->withPivot('price');
当然,当我为特定的 customer_type 请求 customer_types 时,结果不是预期的:
$resources = Material::with(['customer_types' => function($query) use ($customer_type_id)
$query->where('customertype_id', $customer_type_id);
])->get();
它返回了一个带有 customer_types 嵌套集合的 Material 模型,上面有 1 个元素,迫使我使用 first() 或循环 1 个元素。
解决方案:创建一个扩展数据透视表的模型并向其添加关系。
创建了一个扩展枢轴的新模型:
use Illuminate\Database\Eloquent\Relations\Pivot;
class CustomertypeMaterial extends Pivot
protected $table = 'customertype_material';
protected $fillable = ['price', 'customertype_id', 'material_id'];
现在,在我的 Material Model 中添加了一个指向这个新模型的关系:
public function scoped_price()
return $this->belongsTo('App\CustomertypeMaterial', 'id','material_id');
最后加载这个新关系的查询:
$resources = Material::with(['scoped_price' => function($query) use ($customer_type_id)
$query->where('customertype_id', $customer_type_id);
])->get();
结果是一个 Material 模型,其中嵌套了一个 scoped_price 元素并由 customer_type_id 过滤
我不确定这是否是正确的方法,但它对我有用。
希望对你有帮助!
【讨论】:
【参考方案5】:我写了一个更通用的方法来解决这个问题。它的缺点是我必须从 laravel/frame 供应商文件中复制代码。因此,在升级 Laravel 框架时,这可能会在某天中断。
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Str;
/**
* Based on laravel/framework@8.54.0
*/
trait SupportsSingleResultBelongsToMany
/**
* Get the model's relationships in array form.
*
* @return array
*/
public function relationsToArray()
$attributes = [];
foreach ($this->getArrayableRelations() as $key => $value)
// If the values implements the Arrayable interface we can just call this
// toArray method on the instances which will convert both models and
// collections to their proper array form and we'll set the values.
if ($value instanceof Arrayable)
if (isset($this->forceSingleResult) &&
in_array($key, $this->forceSingleResult) &&
$value instanceof \ArrayAccess &&
$value instanceof \Countable
)
$relation = count($value) > 0 ? $value[0] : null;
else
$relation = $value->toArray();
// If the value is null, we'll still go ahead and set it in this list of
// attributes since null is used to represent empty relationships if
// if it a has one or belongs to type relationships on the models.
elseif (is_null($value))
$relation = $value;
// If the relationships snake-casing is enabled, we will snake case this
// key so that the relation attribute is snake cased in this returned
// array to the developers, making this consistent with attributes.
if (static::$snakeAttributes)
$key = Str::snake($key);
// If the relation value has been set, we will set it on this attributes
// list for returning. If it was not arrayable or null, we'll not set
// the value on the array because it is some type of invalid value.
if (isset($relation) || is_null($value))
$attributes[$key] = $relation;
unset($relation);
return $attributes;
然后在您的模型中,只需使用 Trait 并指定哪些关系是单一结果。
class MyModel extends Model
use SupportsSingleResultBelongsToMany;
protected $forceSingleResult = ["teams"];
public function teams()
$this->belongsToMany(Team::class);
【讨论】:
【参考方案6】:Laravel Eloquent 的工作原理是魔法。您可以覆盖魔术方法__get
。如果没有属性,则调用__get
方法:
(在您的模型中)
public function __get ($name)
$answer = parent::__get($name);
if($name=='your_prop')
$answer=!empty($answer)?$answer[0]:null;
return $answer;
如果您的 your_prop
对多关系返回任何值,则取数组中的第一个。
【讨论】:
感谢您的回答。你能解释一下这个答案吗? php.net/manual/en/language.oop5.magic.php Laravel Eloquent 的工作原理是魔法。如果没有属性,则调用__get方法以上是关于Laravel belongsToMany 关系只有一个结果的主要内容,如果未能解决你的问题,请参考以下文章
Laravel 关系查询加载 hasMany + BelongsToMany
Laravel 关系:hasManyThrough、belongsTo、belongsToMany
Laravel:如何编写关于 belongsToMany 关系的连接计数查询?
Laravel 递归 whereHas on BelongsToMany 关系