【问题标题】:Laravel belongsToMany relationship with only one resultLaravel belongsToMany 关系只有一个结果
【发布时间】: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 代码
  • 您的数据透视表是否有其他属性(除了外键)?

标签: laravel eloquent relationship


【解决方案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 对多关系返回任何值,则取数组中的第一个。

        【讨论】:

        猜你喜欢
        • 2021-08-30
        • 2018-05-19
        • 2015-02-22
        • 2020-06-03
        • 1970-01-01
        • 2018-03-08
        • 2021-03-22
        • 2021-10-17
        • 2019-07-08
        相关资源
        最近更新 更多