Laravel - 渴望加载多态关系的相关模型

问题描述 投票:0回答:6

我可以立即加载多态关系/模型,而不会出现任何 n+1 问题。但是,如果我尝试访问与多态模型相关的模型,则会出现 n+1 问题,并且我似乎无法找到解决方案。这是在本地查看它的确切设置:

1)数据库表名/数据

history

history table

companies

enter image description here

products

enter image description here

services

enter image description here

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
。 解决这个问题的有效方法是什么?

laravel eloquent polymorphic-associations eager-loading polymorphism
6个回答
78
投票

解决方案:

这是可能的,如果你添加:

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,但应该可以。


11
投票

您可以分离集合,然后延迟加载每个集合:

$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'];
是很好的解决方案,但这取决于您的业务逻辑。


4
投票

拉取请求 #13737#13741 修复了此问题。

只需更新你的 Laravel 版本和以下代码

protected $with = [‘likeable.owner’];

将按预期工作。


0
投票

我对此不是 100% 确定,因为很难在我的系统中重新创建代码,但也许

belongTo('Company')
应该是
morphedByMany('Company')
。您也可以尝试
morphToMany
。我能够正确加载复杂的多态关系,而无需多次调用。 ?


0
投票

至少从 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 多态关系方法还有很多不足之处。


-1
投票

正如 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);
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.