Laravel - 渴望加载多态关系的相关模型
Posted
技术标签:
【中文标题】Laravel - 渴望加载多态关系的相关模型【英文标题】:Laravel - Eager Loading Polymorphic Relation's Related Models 【发布时间】:2014-12-30 20:49:17 【问题描述】:我可以急切地加载多态关系/模型,而不会出现任何 n+1 问题。但是,如果我尝试访问与多态模型相关的模型,则会出现 n+1 问题,我似乎无法找到解决方法。这是在本地查看的确切设置:
1) 数据库表名/数据
history
companies
products
services
2) 模型
// History
class History extends Eloquent
protected $table = 'history';
public function historable()
return $this->morphTo();
// Company
class Company extends Eloquent
protected $table = 'companies';
// each company has many products
public function products()
return $this->hasMany('Product');
// each company has many services
public function services()
return $this->hasMany('Service');
// Product
class Product extends Eloquent
// each product belongs to a company
public function company()
return $this->belongsTo('Company');
public function history()
return $this->morphMany('History', 'historable');
// Service
class Service extends Eloquent
// each service belongs to a company
public function company()
return $this->belongsTo('Company');
public function history()
return $this->morphMany('History', 'historable');
3) 路由
Route::get('/history', function()
$histories = History::with('historable')->get();
return View::make('historyTemplate', compact('histories'));
);
4) 仅因为 $history->historable->company->name 而记录了 n+1 的模板,将其注释掉,n+1 消失.. 但我们需要那个遥远相关的公司名称:
@foreach($histories as $history)
<p>
<u> $history->historable->company->name </u>
$history->historable->name : $history->historable->status
</p>
@endforeach
dd(DB::getQueryLog());
我需要能够急切地加载公司名称(在单个查询中),因为它是多态关系模型 Product
和 Service
的相关模型。
我已经为此工作了几天,但找不到解决方案。
History::with('historable.company')->get()
只是忽略了historable.company
中的company
。
这个问题的有效解决方案是什么?
【问题讨论】:
你可以通过$historables->load('company')
动态加载Company
模型
@Razor 我添加了一个更新,你的建议会在哪里加载 Company
?
【参考方案1】:
解决方案:
有可能,如果你添加:
protected $with = ['company'];
适用于Service
和Product
型号。这样,company
关系在每次加载 Service
或 Product
时都会被预先加载,包括通过与 History
的多态关系加载时。
说明:
这将导致另外 2 个查询,一个针对 Service
,一个针对 Product
,即每个 historable_type
一个查询。因此,无论n
的结果数量如何,您的查询总数都会从m+1
(没有急切加载遥远的company
关系)到(m*2)+1
,其中m
是链接的模型数量你的多态关系。
可选:
这种方法的缺点是您将总是在Service
和Product
模型上急切加载company
关系。这可能是也可能不是问题,具体取决于您的数据的性质。如果这是一个问题,您可以使用此技巧在调用多态关系时自动预先加载 company
only。
将此添加到您的 History
模型中:
public function getHistorableTypeAttribute($value)
if (is_null($value)) return ($value);
return ($value.'WithCompany');
现在,当您加载 historable
多态关系时,Eloquent 将查找类 ServiceWithCompany
和 ProductWithCompany
,而不是 Service
或 Product
。然后,创建这些类,并在其中设置 with
:
ProductWithCompany.php
class ProductWithCompany extends Product
protected $table = 'products';
protected $with = ['company'];
ServiceWithCompany.php
class ServiceWithCompany extends Service
protected $table = 'services';
protected $with = ['company'];
...最后,您可以从基础 Service
和 Product
类中删除 protected $with = ['company'];
。
有点hacky,但它应该可以工作。
【讨论】:
扩展类的编辑产生了很大的不同,因为它只在多态需要它时调用它,而不是每次都调用它。感谢这个伟大的工作达米亚尼。由于它扩展了原始类,我们可以从扩展类中删除$table
。我再次希望 Taylor 允许我们像其他关系一样轻松查询它$histories = History::with('historable.company')->get();
再次感谢 damiani :)
请注意,只有在父模型上显式声明 $table
时,才能移除它;如果你没有,那么多态调用将寻找表product_with_company
。这种加载距离关系功能将是对 Laravel 的有用补充,尽管它有点复杂,因为它假设相同的关系 (company
) 在所有变形模型上都可用。
是的 - 当然它在父模型中明确说明 :) 我在 Taylor 的 github 上请求它 - 等待看到他对此的想法,因为像其他模型一样渴望加载它会很棒。特别是如果我们显式调用它,如果它存在,它应该工作,否则,抛出一个错误。将是 Laravel 的一个很好的补充。
只是想知道getHistorableTypeAttribute
你是从哪里发现这个的。我似乎无法找到有关它的信息。任何在线资源?
它是一个访问器,在laravel.com/docs/4.2/eloquent#accessors-and-mutators 的文档中有一个非常简短的描述。这是一个在属性(即数据库中的字段)被访问时执行的函数;每当访问 foo
属性时,将执行 getFooAttribute
,并且 $this->foo
将设置为访问器返回的任何值。【参考方案2】:
您可以分离集合,然后延迟加载每个集合:
$histories = History::with('historable')->get();
$productCollection = new Illuminate\Database\Eloquent\Collection();
$serviceCollection = new Illuminate\Database\Eloquent\Collection();
foreach($histories as $history)
if($history->historable instanceof Product)
$productCollection->add($history->historable);
if($history->historable instanceof Service)
$serviceCollection->add($history->historable);
$productCollection->load('company');
$serviceCollection->load('company');
// then merge the two collection if you like
foreach ($serviceCollection as $service)
$productCollection->push($service);
$results = $productCollection;
这可能不是最好的解决方案,按照@damiani 的建议添加protected $with = ['company'];
也是很好的解决方案,但这取决于您的业务逻辑。
【讨论】:
很好的答案剃刀。这和 damiani 的答案都是很好的解决方法,并且两者都有效,但我喜欢 damiani 如何将其提取出来作为保持路由/控制器清洁的类扩展,所以我会寻求他的答案。我真的希望 Taylor Otwell 能够将语法添加到 laravel 并像这样简单地调用它$histories = History::with('historable.company')->get();
感谢您的出色回答 - 赞成!【参考方案3】:
Pull Request #13737 和 #13741 修复了这个问题。
只需更新您的 Laravel 版本和以下代码
protected $with = [‘likeable.owner’];
会按预期工作。
【讨论】:
【参考方案4】:我对此不是 100% 确定,因为在我的系统中重新创建您的代码很困难,但也许 belongTo('Company')
应该是 morphedByMany('Company')
。你也可以试试morphToMany
。我能够获得一个复杂的多态关系来正确加载而无需多次调用。 ?
【讨论】:
不确定,我会在明天早些时候整理更多细节,以便人们可以轻松地在本地重现问题,这样更容易看到它。让它发挥作用真是太棒了。 完全不确定...morphToMany
和morphedByMany
用于多对多多态关系,但情况并非如此,需要不同的表结构.
更新的问题很容易重现。【参考方案5】:
正如 João Guilherme 所提到的,这已在 5.3 版中得到修复。但是,我发现自己在无法升级的应用程序中遇到了同样的错误。所以我创建了一个覆盖方法,将修复应用到 Legacy API。 (感谢 João 为我指明了制作这个的正确方向。)
首先,创建您的 Override 类:
namespace App\Overrides\Eloquent;
use Illuminate\Database\Eloquent\Relations\MorphTo as BaseMorphTo;
/**
* Class MorphTo
* @package App\Overrides\Eloquent
*/
class MorphTo extends BaseMorphTo
/**
* Laravel < 5.2 polymorphic relationships fail to adopt anything from the relationship except the table. Meaning if
* the related model specifies a different database connection, or timestamp or deleted_at Constant definitions,
* they get ignored and the query fails. This was fixed as of Laravel v5.3. This override applies that fix.
*
* Derived from https://github.com/laravel/framework/pull/13741/files and
* https://github.com/laravel/framework/pull/13737/files. And modified to cope with the absence of certain 5.3
* helper functions.
*
* @inheritdoc
*/
protected function getResultsByType($type)
$model = $this->createModelByType($type);
$whereBindings = \Illuminate\Support\Arr::get($this->getQuery()->getQuery()->getRawBindings(), 'where', []);
return $model->newQuery()->withoutGlobalScopes($this->getQuery()->removedScopes())
->mergeWheres($this->getQuery()->getQuery()->wheres, $whereBindings)
->with($this->getQuery()->getEagerLoads())
->whereIn($model->getTable().'.'.$model->getKeyName(), $this->gatherKeysByType($type))->get();
接下来,您需要让您的 Model 类真正与您的 MorphTo 化身而不是 Eloquent 的化身对话。这可以通过应用到每个模型的 trait 或 Illuminate\Database\Eloquent\Model 的子节点来完成,该子节点由模型类而不是 Illuminate\Database\Eloquent\Model 直接扩展。我选择把它变成一个特质。但是,如果您选择将其设置为子类,我将在它推断名称的部分中留下您需要考虑的提示:
<?php
namespace App\Overrides\Eloquent\Traits;
use Illuminate\Support\Str;
use App\Overrides\Eloquent\MorphTo;
/**
* Intended for use inside classes that extend Illuminate\Database\Eloquent\Model
*
* Class MorphPatch
* @package App\Overrides\Eloquent\Traits
*/
trait MorphPatch
/**
* The purpose of this override is just to call on the override for the MorphTo class, which contains a Laravel 5.3
* fix. Functionally, this is otherwise identical to the original method.
*
* @inheritdoc
*/
public function morphTo($name = null, $type = null, $id = null)
//parent::morphTo similarly infers the name, but with a now-erroneous assumption of where in the stack to look.
//So in case this App's version results in calling it, make sure we're explicit about the name here.
if (is_null($name))
$caller = last(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2));
$name = Str::snake($caller['function']);
//If the app using this trait is already at Laravel 5.3 or higher, this override is not necessary.
if (version_compare(app()::VERSION, '5.3', '>='))
return parent::morphTo($name, $type, $id);
list($type, $id) = $this->getMorphs($name, $type, $id);
if (empty($class = $this->$type))
return new MorphTo($this->newQuery(), $this, $id, null, $type, $name);
$instance = new $this->getActualClassNameForMorph($class);
return new MorphTo($instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name);
【讨论】:
以上是关于Laravel - 渴望加载多态关系的相关模型的主要内容,如果未能解决你的问题,请参考以下文章