web-dev-qa-db-ja.com

Laravel-多相リレーションの関連モデルを積極的にロード

ポリモーフィックな関係/モデルをn + 1の問題なしで熱心に読み込むことができます。ただし、ポリモーフィックモデルに関連するモデルにアクセスしようとすると、n + 1の問題が表示され、修正が見つからないようです。ローカルに表示するための正確なセットアップは次のとおりです。

1)DBテーブル名/データ

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');
    }
}
_

)ルーティング

_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の関連モデルであるため、会社名を(1回のクエリで)熱心にロードできる必要があります。私はこれに数日間取り組んでいますが、解決策が見つかりません。 History::with('historable.company')->get()は_historable.company_のcompanyを無視します。この問題の効率的な解決策は何でしょうか?

27
Wonka

解決策:

以下を追加すると可能です。

protected $with = ['company']; 

ServiceおよびProductモデルの両方に。これにより、companyとの多相リレーションを介してロードされる場合を含め、ServiceまたはProductがロードされるたびに、Historyリレーションが積極的にロードされます。


説明:

これにより、Service用とProduct用の2つのクエリ、つまり各historable_typeに対して1つのクエリが追加されます。したがって、クエリの合計数は、結果の数nに関係なく、m+1(遠くのcompany関係を積極的にロードすることなく)から(m*2)+1に移動します。ここで、mは、多態性関係。


オプション:

このアプローチの欠点は、companyおよびServiceモデルでProductリレーションをalways積極的にロードすることです。これは、データの性質に応じて、問題になる場合とそうでない場合があります。これが問題になる場合は、このトリックを使用して、ポリモーフィックリレーションを呼び出すときにcompanyonlyを自動的にeager-loadできます。

これを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'];
}

...そして最後に、ベースのServiceクラスとProductクラスからprotected $with = ['company'];を削除できます。

少しハッキーですが、動作するはずです。

58
damiani

コレクションを分離してから、各コレクションを怠laに熱心にロードできます。

$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;

おそらく最適なソリューションではなく、protected $with = ['company']; @damianiによって提案されているように、良いソリューションですが、ビジネスロジックに依存します。

10
Razor

プルリクエスト #13737 および #13741 はこの問題を修正しました。

Laravel=バージョンと次のコードを更新するだけです

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

期待どおりに動作します。

4
João Guilherme

JoãoGuilhermeが述べたように、これはバージョン5.3で修正されましたが、アップグレードが不可能なアプリでも同じバグに直面していることに気付きました。そこで、修正をレガシーAPIに適用するオーバーライドメソッドを作成しました。 (João、これを作成する正しい方向に私を向けてくれてありがとう。)

まず、オーバーライドクラスを作成します。

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クラスがEloquentの代わりにMorphToの化身と実際に対話できるようにするものが必要になります。これは、各モデルに適用される特性、または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);
    }
}
0
Claymore

私のシステムでコードを再作成するのは難しいので、これについては100%確信はありませんが、おそらくbelongTo('Company')morphedByMany('Company')であるはずです。 morphToManyを試すこともできます。複雑なポリモーフィックな関係を取得して、複数の呼び出しなしで適切にロードすることができました。 ?

0
dwenaus