Laravel动态关系-急于加载时访问模型属性 [英] Laravel dynamic relationships - access model attributes on eager load

查看:64
本文介绍了Laravel动态关系-急于加载时访问模型属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在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,甚至无法运行该查询.

如果这不可能,那么还有什么其他方法可以解决此类问题呢?看来这不可能通过传统的多态关系来解决.我只有两个可能的想法:

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

老实说,我不知道我将如何实现这两个目标.我要这样做正确吗?有什么我要忽略的吗?


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

解决方案

由于渴望加载的工作方式,对于运行SQL来完成您要查找的内容,您无能为力. /p>

当您执行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);
    }
}

警告,没有经过测试.

I have an Eloquent relationship on my Laravel model which is dynamic - that is, the value of a particular database field determines which model will get loaded. I am able to load this relationship fine when I first instantiate the model instance and then reference the relation, but it does not work when I eager load that relationship.

Specifically, I have a Product model. That product might or might not be a parent to another product. If the parent_id of the Product is set to 0 then that product is considered a parent part (whether or not it has children). If the parent_id is set to a different product's id, then that product is a child. I need to be able to access Product::with('parent') and know that the parent relation will return with either itself (yes, duplicated data) or a different product if it is a child.

Here is my relationship so far:

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

When I am eager loading, $this->parent_id is always undefined and therefore this relation will only ever return itself even if it actually is a parent product.

Is there any way to access a model's attributes before a relation is eager loaded? I thought about working in a separate query before I return the relation, but I realized that I don't have access to the product's id to even run that query.

If that is not possible, what are some other ways to address this type of problem? It does not appear that this could be solved through a traditional polymorphic relationship. I only have two possible ideas:

  • Add some sort of constraint to the belongsTo relation where I dynamically determine a foreign key.
  • Create my own custom relationship that uses a foreign key based on a different database field.

I honestly have no idea how I would implement either of those. Am I going about this the right way? Is there something I am overlooking?


After thinking this over more, I think the simplest way to put the question is: is there any way to dynamically select a foreign key for a relationship inside the relation itself at runtime? My use cases don't allow for me to use eager loading constraints when I call the relation - the constraints need to apply to the relation itself.

解决方案

Because of the way eager loading works, there isn't anything you can really do to the SQL being run to get done what you're looking for.

When you do Product::with('parent')->get(), it runs two queries.

First, it runs the query to get all the products:

select * from `products`

Next, it runs a query to get the eager loaded parents:

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

The number of parameters (?) corresponds to the number of results from the first query. Once the second set of models has been retrieved, the match() function is used to relate the objects to each other.

In order to do what you want, you're going to have to create a new relationship and override the match() method. This will handle the eager loading aspect. Additionally, you'll need to override the addConstraints method to handle the lazy loading aspect.

First, create a custom relationship class:

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;
    }
}

Once you've created your custom relationship class, you need to update your model to use this custom relationship. Create a new method on your model that will use your new CustomBelongsTo relationship, and update your parent() relationship method to use this new method, instead of the base belongsTo() method.

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);
    }
}

Fair warning, none of this has been tested.

这篇关于Laravel动态关系-急于加载时访问模型属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆