我可以在没有任何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)n + 1模板仅因为$ history-> historable-> company->名称而被记录,注释掉,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()
只会忽略company
中的historable.company
。
这个问题的有效解决方案是什么?
答案 0 :(得分:53)
<强>解决方案:强>
如果你添加:
,这是可能的protected $with = ['company'];
同时使用Service
和Product
模型。这样,每次加载company
或Service
时都会加载Product
关系,包括通过与History
的多态关系加载时。
<强>解释强>
这会产生额外的2个查询,一个用于Service
,另一个用于Product
,即每个historable_type
一个查询。因此,无论结果数量n
如何,您的查询总数都会从m+1
(没有急切加载远距离company
关系)转到(m*2)+1
,其中m
1}}是由多态关系链接的模型数。
可选强>
这种方法的缺点是,总是在company
和Service
模型上急切加载Product
关系。这可能是也可能不是问题,具体取决于数据的性质。如果这是一个问题,您可以使用此技巧在调用多态关系时自动加载company
。
将此添加到您的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'];
}
...最后,您可以从基础protected $with = ['company'];
和Service
类中删除Product
。
有点hacky,但它应该工作。
答案 1 :(得分:8)
你可以分开收集,然后懒得加载每一个:
$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'];
是一个很好的解决方案,但这取决于你的业务逻辑。
答案 2 :(得分:4)
答案 3 :(得分:0)
我对此并不是100%肯定,因为在我的系统中重新创建代码很困难,但belongTo('Company')
可能应该是morphedByMany('Company')
。您也可以尝试morphToMany
。我能够获得复杂的多态关系,无需多次调用即可正确加载。 ?
答案 4 :(得分:0)
正如JoãoGuilherme所提到的,这在版本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的化身交谈的东西。这可以通过应用于每个模型的特征,或者由模型类而不是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);
}
}