【问题标题】:Laravel - Eager Loading Polymorphic Relation's Related ModelsLaravel - 渴望加载多态关系的相关模型
【发布时间】: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

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


【解决方案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);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-02-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-19
      • 2014-04-19
      • 2015-05-16
      • 1970-01-01
      相关资源
      最近更新 更多