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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-14 10:22:24  来源:igfitidea点击:

Laravel - Eager Loading Polymorphic Relation's Related Models

laraveleloquentpolymorphic-associationseager-loadingpolymorphism

提问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

history table

companies

历史表

companies

enter image description here

products

在此处输入图片说明

products

enter image description here

services

在此处输入图片说明

services

enter image description here

在此处输入图片说明

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 Productand Service. I've been working on this for days but can't find a solution. History::with('historable.company')->get()just ignores the companyin historable.company. What would an efficient solution to this problem be?

我需要能够急切地(在单个查询中)加载公司名称,因为它是多态关系模型ProductService. 我已经为此工作了几天,但找不到解决方案。 History::with('historable.company')->get()只是忽略companyin historable.company。这个问题的有效解决方案是什么?

回答by damiani

Solution:

解决方案:

It is possible, if you add:

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

protected $with = ['company']; 

to both the Serviceand Productmodels. That way, the companyrelation is eager-loaded every time a Serviceor a Productis loaded, including when loaded via the polymorphic relation with History.

到两个ServiceProduct模型。这样,company每次加载 aService或 aProduct时都会立即加载该关系,包括通过与 的多态关系加载时History



Explanation:

解释:

This will result in an additional 2 queries, one for Serviceand 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 companyrelation) to (m*2)+1, where mis 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 companyrelation on the Serviceand Productmodels. 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 companyonlywhen calling the polymorphic relation.

这种方法的缺点是你总是会急切加载和模型company上的关系。这可能是也可能不是问题,具体取决于您的数据的性质。如果这是一个问题,您可以使用此技巧在调用多态关系时自动预先加载。ServiceProductcompany

Add this to your Historymodel:

将此添加到您的History模型中:

public function getHistorableTypeAttribute($value)
{
    if (is_null($value)) return ($value); 
    return ($value.'WithCompany');
}

Now, when you load the historablepolymorphic relation, Eloquent will look for the classes ServiceWithCompanyand ProductWithCompany, rather than Serviceor Product. Then, create those classes, and set withinside them:

现在,当您加载historable多态关系时,Eloquent 将查找类ServiceWithCompanyand ProductWithCompany,而不是Serviceor 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 Serviceand Productclasses.

...最后,您可以protected $with = ['company'];从基类ServiceProduct类中删除。

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

Pull Request #13737and #13741fixed this issue.

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

Just update your Laravel version and the following code

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

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

Will work as expected.

将按预期工作。

回答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。我能够在没有多次调用的情况下获得复杂的多态关系以正确加载。?