首页 文章

Laravel动态关系 - 在渴望加载时访问模型属性

提问于
浏览
2

我在Laravel模型上有一个雄辩的关系,它是动态的 - 也就是说,特定数据库字段的值决定了哪个模型将被加载 . 当我第一次实例化模型实例然后引用关系时,我能够很好地加载这种关系,但是当我急切地加载这种关系时它不起作用 .

具体来说,我有一个 Product 模型 . 该产品可能是也可能不是其他产品的父母 . 如果产品的 parent_id 设置为 0 ,则该产品被视为父产品(无论是否有子产品) . 如果 parent_id 设置为其他产品的ID,则该产品是子产品 . 我需要能够访问 Product::with('parent') 并知道 parent 关系将返回其自身(是,重复数据)或不同的产品(如果它是孩子) .

这是我到目前为止的关系:

public function parent()
{
    if ($this->parent_id > 0) {
        return $this->belongsTo('App\Product', 'parent_id', 'id');
    } else {
        return $this->belongsTo('App\Product', 'id', 'id');
    }
}

当我急切地加载时, $this->parent_id 总是未定义的,因此即使它实际上是父产品,这种关系也只会自行返回 .

在关系被急切加载之前,有没有办法访问模型的属性?在我返回关系之前,我考虑过在单独的查询中工作,但我意识到我甚至没有运行该查询的id .1121236_的id .

如果不可能,还有哪些其他方法可以解决这类问题?这似乎不能通过传统的多态关系来解决 . 我只有两个可能的想法:

  • 为我动态确定外键的 belongsTo 关系添加某种约束 .

  • 创建我自己的自定义关系,该关系使用基于不同数据库字段的外键 .

老实说,我不知道如何实现其中任何一个 . 我是以正确的方式来做这件事的吗?有什么我可以忽略的吗?


在考虑了这个之后,我认为最简单的问题是: is there any way to dynamically select a foreign key for a relationship inside the relation itself at runtime ?当我调用关系时,我的用例不允许我使用急切的加载约束 - 约束需要应用于关系本身 .

1 回答

  • 5

    由于渴望加载的方式起作用,因此没有任何东西可以用来运行SQL来完成你正在寻找的东西 .

    当你执行 Product::with('parent')->get() 时,它会运行两个查询 .

    首先,它运行查询以获取所有产品:

    select * from `products`
    

    接下来,它运行一个查询来获取渴望加载的父项:

    select * from `products` where `products`.`id` in (?, ?, ?)
    

    参数数量( ? )对应于第一个查询的结果数 . 一旦检索到第二组模型, match() 函数用于将对象相互关联 .

    为了做你想做的事,你将不得不创建一个新的关系并覆盖 match() 方法 . 这将处理急切的加载方面 . 此外,您还需要覆盖 addConstraints 方法来处理延迟加载方面 .

    首先,创建一个自定义关系类:

    class CustomBelongsTo extends BelongsTo
    {
        // Override the addConstraints method for the lazy loaded relationship.
        // If the foreign key of the model is 0, change the foreign key to the
        // model's own key, so it will load itself as the related model.
    
        /**
         * Set the base constraints on the relation query.
         *
         * @return void
         */
        public function addConstraints()
        {
            if (static::$constraints) {
                // For belongs to relationships, which are essentially the inverse of has one
                // or has many relationships, we need to actually query on the primary key
                // of the related models matching on the foreign key that's on a parent.
                $table = $this->related->getTable();
    
                $key = $this->parent->{$this->foreignKey} == 0 ? $this->otherKey : $this->foreignKey;
    
                $this->query->where($table.'.'.$this->otherKey, '=', $this->parent->{$key});
            }
        }
    
        // Override the match method for the eager loaded relationship.
        // Most of this is copied from the original method. The custom
        // logic is in the elseif.
    
        /**
         * Match the eagerly loaded results to their parents.
         *
         * @param  array   $models
         * @param  \Illuminate\Database\Eloquent\Collection  $results
         * @param  string  $relation
         * @return array
         */
        public function match(array $models, Collection $results, $relation)
        {
            $foreign = $this->foreignKey;
    
            $other = $this->otherKey;
    
            // First we will get to build a dictionary of the child models by their primary
            // key of the relationship, then we can easily match the children back onto
            // the parents using that dictionary and the primary key of the children.
            $dictionary = [];
    
            foreach ($results as $result) {
                $dictionary[$result->getAttribute($other)] = $result;
            }
    
            // Once we have the dictionary constructed, we can loop through all the parents
            // and match back onto their children using these keys of the dictionary and
            // the primary key of the children to map them onto the correct instances.
            foreach ($models as $model) {
                if (isset($dictionary[$model->$foreign])) {
                    $model->setRelation($relation, $dictionary[$model->$foreign]);
                }
                // If the foreign key is 0, set the relation to a copy of the model
                elseif($model->$foreign == 0) {
                    // Make a copy of the model.
                    // You don't want recursion in your relationships.
                    $copy = clone $model;
    
                    // Empty out any existing relationships on the copy to avoid
                    // any accidental recursion there.
                    $copy->setRelations([]);
    
                    // Set the relation on the model to the copy of itself.
                    $model->setRelation($relation, $copy);
                }
            }
    
            return $models;
        }
    }
    

    创建自定义关系类后,需要更新模型以使用此自定义关系 . 在模型上创建一个将使用新的 CustomBelongsTo 关系的新方法,并更新 parent() 关系方法以使用此新方法,而不是基本 belongsTo() 方法 .

    class Product extends Model
    {
    
        // Update the parent() relationship to use the custom belongsto relationship
        public function parent()
        {
            return $this->customBelongsTo('App\Product', 'parent_id', 'id');
        }
    
        // Add the method to create the CustomBelongsTo relationship. This is
        // basically a copy of the base belongsTo method, but it returns
        // a new CustomBelongsTo relationship instead of the original BelongsTo relationship
        public function customBelongsTo($related, $foreignKey = null, $otherKey = null, $relation = null)
        {
            // If no relation name was given, we will use this debug backtrace to extract
            // the calling method's name and use that as the relationship name as most
            // of the time this will be what we desire to use for the relationships.
            if (is_null($relation)) {
                list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
    
                $relation = $caller['function'];
            }
    
            // If no foreign key was supplied, we can use a backtrace to guess the proper
            // foreign key name by using the name of the relationship function, which
            // when combined with an "_id" should conventionally match the columns.
            if (is_null($foreignKey)) {
                $foreignKey = Str::snake($relation).'_id';
            }
    
            $instance = new $related;
    
            // Once we have the foreign key names, we'll just create a new Eloquent query
            // for the related models and returns the relationship instance which will
            // actually be responsible for retrieving and hydrating every relations.
            $query = $instance->newQuery();
    
            $otherKey = $otherKey ?: $instance->getKeyName();
    
            return new CustomBelongsTo($query, $this, $foreignKey, $otherKey, $relation);
        }
    }
    

    公平警告,这些都没有经过测试 .

相关问题