Laravel 动态关系 - 在急切加载时访问模型属性
Laravel dynamic relationships - access model attributes on eager load
我的 Laravel 模型有一个 Eloquent 关系,它是动态的 - 也就是说,特定数据库字段的值决定将加载哪个模型。当我第一次实例化模型实例然后引用关系时,我能够很好地加载这个关系,但是当我急切地加载那个关系时它不起作用。
具体来说,我有一个 Product
模型。该产品可能是也可能不是另一个产品的父产品。如果产品的 parent_id
设置为 0
,则该产品被视为父部件(无论它是否有子部件)。如果 parent_id
设置为不同的产品 ID,则该产品是子产品。我需要能够访问 Product::with('parent')
并且知道 parent
关系将 return 与 或者 本身(是的,重复的数据)或不同的如果是儿童产品。
这是我目前的关系:
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
始终未定义,因此即使它实际上是父产品,这种关系也只会 return 本身。
有什么方法可以在关系被预先加载之前访问模型的属性吗?在我 return 关系之前,我考虑过在单独的查询中工作,但我意识到我什至无法访问产品的 ID 甚至 运行 该查询。
如果那不可能,还有哪些其他方法可以解决此类问题?这似乎无法通过传统的多态关系来解决。我只有两个可能的想法:
- 向动态确定外键的
belongsTo
关系添加某种约束。
- 创建我自己的自定义关系,该关系使用基于不同数据库字段的外键。
老实说,我不知道我将如何实施其中任何一个。我这样做是对的吗?我忽略了什么吗?
在仔细考虑之后,我认为提出问题的最简单方法是:有没有办法动态地 select 关系本身内的关系的外键 [= =55=]时间?我的用例不允许我在调用关系时使用预加载约束 - 约束需要应用于关系本身。
由于预加载的工作方式,您无法真正做到 SQL 运行 完成您想要的。
当您执行 Product::with('parent')->get()
时,它会运行两个查询。
首先,它运行查询以获取所有产品:
select * from `products`
接下来,它运行查询以获取预加载的父级:
select * from `products` where `products`.`id` in (?, ?, ?)
参数数量 (?
) 对应于第一个查询的结果数量。检索到第二组模型后,match()
函数用于将对象相互关联。
为了做你想做的事,你将不得不创建一个新的关系并覆盖 match()
方法。这将处理预加载方面。此外,您需要覆盖 addConstraints
方法来处理延迟加载方面。
首先,创建自定义关系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;
}
}
创建自定义关系后 class,您需要更新模型以使用此自定义关系。在您的模型上创建一个将使用您的新 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);
}
}
公平警告,none 已经过测试。
我的 Laravel 模型有一个 Eloquent 关系,它是动态的 - 也就是说,特定数据库字段的值决定将加载哪个模型。当我第一次实例化模型实例然后引用关系时,我能够很好地加载这个关系,但是当我急切地加载那个关系时它不起作用。
具体来说,我有一个 Product
模型。该产品可能是也可能不是另一个产品的父产品。如果产品的 parent_id
设置为 0
,则该产品被视为父部件(无论它是否有子部件)。如果 parent_id
设置为不同的产品 ID,则该产品是子产品。我需要能够访问 Product::with('parent')
并且知道 parent
关系将 return 与 或者 本身(是的,重复的数据)或不同的如果是儿童产品。
这是我目前的关系:
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
始终未定义,因此即使它实际上是父产品,这种关系也只会 return 本身。
有什么方法可以在关系被预先加载之前访问模型的属性吗?在我 return 关系之前,我考虑过在单独的查询中工作,但我意识到我什至无法访问产品的 ID 甚至 运行 该查询。
如果那不可能,还有哪些其他方法可以解决此类问题?这似乎无法通过传统的多态关系来解决。我只有两个可能的想法:
- 向动态确定外键的
belongsTo
关系添加某种约束。 - 创建我自己的自定义关系,该关系使用基于不同数据库字段的外键。
老实说,我不知道我将如何实施其中任何一个。我这样做是对的吗?我忽略了什么吗?
在仔细考虑之后,我认为提出问题的最简单方法是:有没有办法动态地 select 关系本身内的关系的外键 [= =55=]时间?我的用例不允许我在调用关系时使用预加载约束 - 约束需要应用于关系本身。
由于预加载的工作方式,您无法真正做到 SQL 运行 完成您想要的。
当您执行 Product::with('parent')->get()
时,它会运行两个查询。
首先,它运行查询以获取所有产品:
select * from `products`
接下来,它运行查询以获取预加载的父级:
select * from `products` where `products`.`id` in (?, ?, ?)
参数数量 (?
) 对应于第一个查询的结果数量。检索到第二组模型后,match()
函数用于将对象相互关联。
为了做你想做的事,你将不得不创建一个新的关系并覆盖 match()
方法。这将处理预加载方面。此外,您需要覆盖 addConstraints
方法来处理延迟加载方面。
首先,创建自定义关系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;
}
}
创建自定义关系后 class,您需要更新模型以使用此自定义关系。在您的模型上创建一个将使用您的新 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);
}
}
公平警告,none 已经过测试。