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

Posted

技术标签:

【中文标题】Laravel - 渴望加载多态关系的相关模型【英文标题】:Laravel - Eager Loading Polymorphic Relation's Related Models 【发布时间】:2014-12-30 20:49:17 【问题描述】:

我可以急切地加载多态关系/模型,而不会出现任何 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()); 

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

【问题讨论】:

你可以通过$historables-&gt;load('company')动态加载Company模型 @Razor 我添加了一个更新,你的建议会在哪里加载 Company 【参考方案1】:

解决方案:

有可能,如果你添加:

protected $with = ['company']; 

适用于ServiceProduct 型号。这样,company 关系在每次加载 ServiceProduct 时都会被预先加载,包括通过与 History 的多态关系加载时。


说明:

这将导致另外 2 个查询,一个针对 Service,一个针对 Product,即每个 historable_type 一个查询。因此,无论n 的结果数量如何,您的查询总数都会从m+1(没有急切加载遥远的company 关系)到(m*2)+1,其中m 是链接的模型数量你的多态关系。


可选:

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

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

public function getHistorableTypeAttribute($value)

    if (is_null($value)) return ($value); 
    return ($value.'WithCompany');

现在,当您加载 historable 多态关系时,Eloquent 将查找类 ServiceWithCompanyProductWithCompany,而不是 ServiceProduct。然后,创建这些类,并在其中设置 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'];

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

有点hacky,但它应该可以工作。

【讨论】:

扩展类的编辑产生了很大的不同,因为它只在多态需要它时调用它,而不是每次都调用它。感谢这个伟大的工作达米亚尼。由于它扩展了原始类,我们可以从扩展类中删除$table。我再次希望 Taylor 允许我们像其他关系一样轻松查询它$histories = History::with('historable.company')-&gt;get(); 再次感谢 damiani :) 请注意,只有在父模型上显式声明 $table 时,才能移除它;如果你没有,那么多态调用将寻找表product_with_company。这种加载距离关系功能将是对 Laravel 的有用补充,尽管它有点复杂,因为它假设相同的关系 (company) 在所有变形模型上都可用。 是的 - 当然它在父模型中明确说明 :) 我在 Taylor 的 github 上请求它 - 等待看到他对此的想法,因为像其他模型一样渴望加载它会很棒。特别是如果我们显式调用它,如果它存在,它应该工作,否则,抛出一个错误。将是 Laravel 的一个很好的补充。 只是想知道getHistorableTypeAttribute 你是从哪里发现这个的。我似乎无法找到有关它的信息。任何在线资源? 它是一个访问器,在laravel.com/docs/4.2/eloquent#accessors-and-mutators 的文档中有一个非常简短的描述。这是一个在属性(即数据库中的字段)被访问时执行的函数;每当访问 foo 属性时,将执行 getFooAttribute,并且 $this-&gt;foo 将设置为访问器返回的任何值。【参考方案2】:

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

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

【讨论】:

很好的答案剃刀。这和 damiani 的答案都是很好的解决方法,并且两者都有效,但我喜欢 damiani 如何将其提取出来作为保持路由/控制器清洁的类扩展,所以我会寻求他的答案。我真的希望 Taylor Otwell 能够将语法添加到 laravel 并像这样简单地调用它 $histories = History::with('historable.company')-&gt;get(); 感谢您的出色回答 - 赞成!【参考方案3】:

Pull Request #13737 和 #13741 修复了这个问题。

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

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

会按预期工作。

【讨论】:

【参考方案4】:

我对此不是 100% 确定,因为在我的系统中重新创建您的代码很困难,但也许 belongTo('Company') 应该是 morphedByMany('Company')。你也可以试试morphToMany。我能够获得一个复杂的多态关系来正确加载而无需多次调用。 ?

【讨论】:

不确定,我会在明天早些时候整理更多细节,以便人们可以轻松地在本地重现问题,这样更容易看到它。让它发挥作用真是太棒了。 完全不确定...morphToManymorphedByMany 用于多对多多态关系,但情况并非如此,需要不同的表结构. 更新的问题很容易重现。【参考方案5】:

正如 João Guilherme 所提到的,这已在 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 的化身对话。这可以通过应用到每个模型的 trait 或 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);
    

【讨论】:

以上是关于Laravel - 渴望加载多态关系的相关模型的主要内容,如果未能解决你的问题,请参考以下文章

Laravel Eloquent:渴望加载多个嵌套关系

Laravel 渴望加载数据透视表关系

Laravel 5 具有多态关系的只读视图模型

如何最好地链接到 Laravel 中的多态关系

laravel 关联模型 多态关系

渴望加载 Laravel 5 与两个外键的多对多关系