Laravel belongsToMany 关系只有一个结果

Posted

技术标签:

【中文标题】Laravel belongsToMany 关系只有一个结果【英文标题】:Laravel belongsToMany relationship with only one result 【发布时间】:2019-08-30 16:10:20 【问题描述】:

我在两个表之间建立了一个关系,其中一个连接表只有一个结果。

当我定义 Laravel 的 belongsToMany 关系时,我希望它单独返回该项目,而不是返回一个只有一个元素的集合。

有没有办法在 Laravel 中对此进行建模?

提前致谢。

[编辑]

我将尝试使用经典的用户/角色示例来解释我想要什么。除了usersroles 表之外,我们还有一个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 关系

Laravel - Eager Loading BelongsToMany 关系

Laravel 5.4急切加载belongsToMany关系null绑定