Laravel - 热切加载多态关系的相关模型
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/26727088/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Laravel - Eager Loading Polymorphic Relation's Related Models
提问by Wonka
I can eager load polymorphic relations/models without any n+1 issues. However, if I try to access a model related to the polymorphic model, the n+1 problem appears and I can't seem to find a fix. Here is the exact setup to see it locally:
我可以急切地加载多态关系/模型而没有任何 n+1 问题。但是,如果我尝试访问与多态模型相关的模型,则会出现 n+1 问题,而且我似乎无法找到解决方法。这是在本地查看它的确切设置:
1) DB table name/data
1) 数据库表名/数据
history
history
companies
companies
products
products
services
services
2) Models
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) Routing
3) 路由
Route::get('/history', function(){
$histories = History::with('historable')->get();
return View::make('historyTemplate', compact('histories'));
});
4) Template with n+1 logged only becacuse of $history->historable->company->name, comment it out, n+1 goes away.. but we need that distant related company name:
4) n+1 的模板只是因为 $history->historable->company->name 才被记录下来,注释掉,n+1 消失了.. 但我们需要那个远相关的公司名称:
@foreach($histories as $history)
<p>
<u>{{ $history->historable->company->name }}</u>
{{ $history->historable->name }}: {{ $history->historable->status }}
</p>
@endforeach
{{ dd(DB::getQueryLog()); }}
I need to be able to load the company names eagerly (in a single query) as it's a related model of the polymorphic relation models Product
and Service
.
I've been working on this for days but can't find a solution.
History::with('historable.company')->get()
just ignores the company
in historable.company
.
What would an efficient solution to this problem be?
我需要能够急切地(在单个查询中)加载公司名称,因为它是多态关系模型Product
和Service
. 我已经为此工作了几天,但找不到解决方案。
History::with('historable.company')->get()
只是忽略company
in historable.company
。这个问题的有效解决方案是什么?
回答by damiani
Solution:
解决方案:
It is possible, if you add:
这是可能的,如果你添加:
protected $with = ['company'];
to both the Service
and Product
models. That way, the company
relation is eager-loaded every time a Service
or a Product
is loaded, including when loaded via the polymorphic relation with History
.
到两个Service
及Product
模型。这样,company
每次加载 aService
或 aProduct
时都会立即加载该关系,包括通过与 的多态关系加载时History
。
Explanation:
解释:
This will result in an additional 2 queries, one for Service
and one for Product
, i.e. one query for each historable_type
. So your total number of queries—regardless of the number of results n
—goes from m+1
(without eager-loading the distant company
relation) to (m*2)+1
, where m
is the number of models linked by your polymorphic relation.
这将导致额外的 2 个查询,一个 forService
和一个 for Product
,即每个查询一个historable_type
。因此,无论结果数量如何,您的查询n
总数从m+1
(不急切加载远距离company
关系)到(m*2)+1
,其中m
是由您的多态关系链接的模型数量。
Optional:
可选的:
The downside of this approach is that you will alwayseager-load the company
relation on the Service
and Product
models. This may or may not be an issue, depending on the nature of your data. If this is a problem, you could use this trick to automatically eager-load company
onlywhen calling the polymorphic relation.
这种方法的缺点是你总是会急切加载和模型company
上的关系。这可能是也可能不是问题,具体取决于您的数据的性质。如果这是一个问题,您可以使用此技巧仅在调用多态关系时自动预先加载。Service
Product
company
Add this to your History
model:
将此添加到您的History
模型中:
public function getHistorableTypeAttribute($value)
{
if (is_null($value)) return ($value);
return ($value.'WithCompany');
}
Now, when you load the historable
polymorphic relation, Eloquent will look for the classes ServiceWithCompany
and ProductWithCompany
, rather than Service
or Product
. Then, create those classes, and set with
inside them:
现在,当您加载historable
多态关系时,Eloquent 将查找类ServiceWithCompany
and ProductWithCompany
,而不是Service
or Product
。然后,创建这些类,并with
在其中设置:
ProductWithCompany.php
ProductWithCompany.php
class ProductWithCompany extends Product {
protected $table = 'products';
protected $with = ['company'];
}
ServiceWithCompany.php
ServiceWithCompany.php
class ServiceWithCompany extends Service {
protected $table = 'services';
protected $with = ['company'];
}
...and finally, you can remove protected $with = ['company'];
from the base Service
and Product
classes.
...最后,您可以protected $with = ['company'];
从基类Service
和Product
类中删除。
A bit hacky, but it should work.
有点hacky,但它应该工作。
回答by Razor
You can separate the collection, then lazy eager load each one:
您可以将集合分开,然后延迟加载每个集合:
$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;
Probably it's not the best solution, adding protected $with = ['company'];
as suggested by @damiani is as good solution, but it depends on your business logic.
可能这不是最好的解决方案,protected $with = ['company'];
按照@damiani 的建议添加也是一个很好的解决方案,但这取决于您的业务逻辑。
回答by Jo?o Guilherme
回答by kmuenkel
As Jo?o Guilherme mentioned, this was fixed in version 5.3 However, I've found myself facing the same bug in an App where it's not feasible to upgrade. So I've created an override method that will apply the fix to Legacy APIs. (Thanks Jo?o, for pointing me in the right direction to produce this.)
正如 Jo?o Guilherme 提到的,这在 5.3 版中得到了修复。但是,我发现自己在应用程序中遇到了同样的错误,无法升级。所以我创建了一个覆盖方法,将修复应用到旧 API。(感谢 Jo?o,为我指出了正确的方向来制作这个。)
First, create your Override class:
首先,创建您的 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();
}
}
Next, you'll need something that lets your Model classes actually talk to your incarnation of MorphTo rather than Eloquent's. This can be done by either a trait applied to each model, or a child of Illuminate\Database\Eloquent\Model that gets extended by your model classes instead of Illuminate\Database\Eloquent\Model directly. I chose to make this into a trait. But in case you chose to make it a child class, I've left in the part where it infers the name as a heads-up that that's something you'd need to consider:
接下来,您需要一些东西,让您的 Model 类实际上与您的 MorphTo 而不是 Eloquent 的化身对话。这可以通过应用于每个模型的特征或 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);
}
}
回答by dwenaus
I'm not 100% sure on this, because it's hard to re-create your code in my system but perhaps belongTo('Company')
should be morphedByMany('Company')
. You could also try morphToMany
. I was able to get a complex polymorphic relationship to load properly without multiple calls. ?
我对此不是 100% 确定,因为很难在我的系统中重新创建您的代码,但也许belongTo('Company')
应该是morphedByMany('Company')
. 你也可以试试morphToMany
。我能够在没有多次调用的情况下获得复杂的多态关系以正确加载。?