我可以立即加载多态关系/模型,而不会出现任何 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()
只是忽略 company
中的 historable.company
。
解决这个问题的有效方法是什么?
解决方案:
这是可能的,如果你添加:
protected $with = ['company'];
适用于
Service
和 Product
型号。这样,每次加载 company
或 Service
时,都会急切加载 Product
关系,包括通过 History
的多态关系加载时。
说明:
这将导致额外 2 个查询,一个针对
Service
,一个针对 Product
,即每个 historable_type
一个查询。因此,您的查询总数(无论结果数量 n
是多少)从 m+1
(无需急切加载远程 company
关系)到 (m*2)+1
,其中 m
是由你的多态关系。
可选:
这种方法的缺点是,您将始终急切地加载
company
和Service
模型上的Product
关系。这可能是也可能不是问题,具体取决于数据的性质。如果这是一个问题,您可以使用此技巧在调用多态关系时自动急切加载 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'];
}
...最后,您可以从基础
protected $with = ['company'];
和 Service
类中删除 Product
。
有点hacky,但应该可以。
您可以分离集合,然后延迟加载每个集合:
$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'];
是很好的解决方案,但这取决于您的业务逻辑。
我对此不是 100% 确定,因为很难在我的系统中重新创建代码,但也许
belongTo('Company')
应该是 morphedByMany('Company')
。您也可以尝试morphToMany
。我能够正确加载复杂的多态关系,而无需多次调用。 ?
至少从 Laravel 7.0 开始,您可以使用
loadMorph
来急切加载嵌套关系:
https://laravel.com/docs/11.x/eloquent-relationships#nested-lazy-eager-loading-morphto
$activities = ActivityFeed::with('parentable')
->get() // notice how loadMorph is not a query builder, but a Collection method!
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
这仍然有一个不幸的缺点,即 Eloquent 没有提供一种默认情况下始终急切加载这些关系的方法,就像您可以使用
protected $with = ['...'];
属性一样。
总的来说,Laravel 多态关系方法还有很多不足之处。
正如 João Guilherme 提到的,这个问题在 5.3 版本中得到了修复。但是,我发现自己在应用程序中遇到了同样的错误,无法升级。 因此,我创建了一个覆盖方法,将修复应用于旧版 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);
}
}