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

时间:2014-11-04 03:06:22

标签: laravel eloquent polymorphic-associations eager-loading polymorphism

我可以在没有任何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)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()); }}

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

5 个答案:

答案 0 :(得分:53)

<强>解决方案:

如果你添加:

,这是可能的
protected $with = ['company']; 

同时使用ServiceProduct模型。这样,每次加载companyService时都会加载Product关系,包括通过与History的多态关系加载时。


<强>解释

这会产生额外的2个查询,一个用于Service,另一个用于Product,即每个historable_type一个查询。因此,无论结果数量n如何,您的查询总数都会从m+1(没有急切加载远距离company关系)转到(m*2)+1,其中m 1}}是由多态关系链接的模型数。


可选

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

将此添加到您的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'];
}

...最后,您可以从基础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)

拉取请求#13737#13741解决了此问题。

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

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

将按预期工作。

答案 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);
    }
}