ポリモーフィックな関係/モデルをn + 1の問題なしで熱心に読み込むことができます。ただし、ポリモーフィックモデルに関連するモデルにアクセスしようとすると、n + 1の問題が表示され、修正が見つからないようです。ローカルに表示するための正確なセットアップは次のとおりです。
1)DBテーブル名/データ
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');
}
}
_
)ルーティング
_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
を無視します。この問題の効率的な解決策は何でしょうか?
解決策:
以下を追加すると可能です。
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積極的にロードすることです。これは、データの性質に応じて、問題になる場合とそうでない場合があります。これが問題になる場合は、このトリックを使用して、ポリモーフィックリレーションを呼び出すときにcompany
onlyを自動的に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'];
を削除できます。
少しハッキーですが、動作するはずです。
コレクションを分離してから、各コレクションを怠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によって提案されているように、良いソリューションですが、ビジネスロジックに依存します。
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);
}
}
私のシステムでコードを再作成するのは難しいので、これについては100%確信はありませんが、おそらくbelongTo('Company')
はmorphedByMany('Company')
であるはずです。 morphToMany
を試すこともできます。複雑なポリモーフィックな関係を取得して、複数の呼び出しなしで適切にロードすることができました。 ?