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

Posted

技术标签:

【中文标题】Laravel 动态关系 - 在急切加载时访问模型属性【英文标题】:Laravel dynamic relationships - access model attributes on eager load 【发布时间】:2016-05-28 01:38:23 【问题描述】:

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

具体来说,我有一个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,甚至无法运行该查询。

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

在我动态确定外键的belongsTo 关系中添加某种约束。 创建我自己的自定义关系,该关系使用基于不同数据库字段的外键。

老实说,我不知道如何实现其中任何一个。我会以正确的方式解决这个问题吗?有什么我忽略的吗?


在考虑了更多之后,我认为提出问题的最简单方法是:有没有办法在运行时为关系本身内部的关系动态选择外键?我的用例不允许我在调用关系时使用急切加载约束 - 约束需要应用于关系本身。

【问题讨论】:

如果不是父级,为什么不将产品自己的 id 放在字段中? 如果我从头开始,我想我会这样做,但不幸的是,我正在为一个庞大的遗留代码库/数据库创建一个 Laravel API 服务。如果我修改了该字段的工作方式,它将破坏其他地方的大量代码。我考虑过添加一个新字段来执行此操作,但我必须在可以在旧站点中输入产品的每个地方添加大量旧代码 - 一个真正的噩梦项目。 我希望有某种方法能够使用 Laravel 来实现这一点。如果每一个替代方案都被证明是不可能的,我会告诉你并让你回答这个问题,这样至少你的回答可能会帮助其他尝试做同样事情的人——即使它对我没有帮助。不过,在放弃之前,我会再给这个问题多一点时间。 有趣的是,您应该提到这一点,目前正在从事类似的项目。我再考虑一下。 Laravel 的关系没有我希望的那么稳固。 也许可以尝试使用 Morph 关系来处理它? 【参考方案1】:

由于急切加载的工作方式,您无法真正对正在运行的 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);
    

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

【讨论】:

哇,非常感谢!我以前从来没有很幸运地建立自己的关系,但这正是我所需要的!但是,我想指出,在特定的用例中,这将不起作用:如果您在嵌套的whereHas 查询中使用它,即Product::whereHas('parent.location', function ($q) //something )->find(1234)。但是,我认为这更多地与这样一个缺点有关,即它在自身上构建子查询,而其他字段需要作为主键,而不是这种关系本身(如果这有意义的话)。只是想在这里添加它以防其他人需要它。谢谢! @AndyNoelker Laravel 在自我关系方面遇到了haswhereHas 的问题。实际上,我最近提交了 5.1 和 5.2 的 PR,它们在修复此问题时已合并。如果您使用的是 5.1,则 5.1.30 版在大约 40 分钟前刚刚被标记,该版本已修复。如果您使用的是 5.2,则 5.2.15 版本在 5 天前被标记,具有修复功能。如果你需要这个 whereHas 功能,你需要 composer update Laravel。 @AndyNoelker 如果您关心血腥细节,可以查看PR #12146。 你是个传奇!是的,该更新也完全解决了whereHas 问题! @AndyNoelker 我做了同样的事情,专注于急切加载并忘记了延迟加载。我很高兴你弄明白了。我用相关信息更新了答案。 SO 上的审阅者通常会拒绝对已发布代码的编辑。仅供参考,如果您在覆盖 addConstraints() 方法时遇到任何问题,您可以尝试覆盖 getResults() 方法。但是,我认为您对当前代码很满意。

以上是关于Laravel 动态关系 - 在急切加载时访问模型属性的主要内容,如果未能解决你的问题,请参考以下文章

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

带有嵌套关系的 Laravel 急切加载

Laravel 自定义数据透视表关系和急切加载?

Laravel 混合获取 Eloquent 急切加载嵌套多个模型

使用 Laravel 将两个模型合并到一个分页查询中,并带有急切加载的关系

laravel eloquent - 在嵌套急切加载的关系上不使用