Je peux ardemment charger des relations/modèles polymorphes sans aucun problème n + 1. Cependant, si j'essaie d'accéder à un modèle lié au modèle polymorphe, le problème n + 1 apparaît et je n'arrive pas à trouver de solution. Voici la configuration exacte pour le voir localement:
1) Nom/données de la table DB
history
companies
products
services
2) Modèles
// 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');
}
}
) Routage
Route::get('/history', function(){
$histories = History::with('historable')->get();
return View::make('historyTemplate', compact('histories'));
});
4) Modèle avec n + 1 connecté uniquement en raison de $ history-> historable-> company-> nom, commentez-le, n + 1 disparaît .. mais nous avons besoin de ce nom de société apparenté éloigné:
@foreach($histories as $history)
<p>
<u>{{ $history->historable->company->name }}</u>
{{ $history->historable->name }}: {{ $history->historable->status }}
</p>
@endforeach
{{ dd(DB::getQueryLog()); }}
Je dois être en mesure de charger les noms de société avec impatience (dans une seule requête) car il s'agit d'un modèle connexe des modèles de relation polymorphe Product
et Service
. J'y travaille depuis des jours mais je ne trouve pas de solution. History::with('historable.company')->get()
ignore simplement le company
dans historable.company
. Quelle serait une solution efficace à ce problème?
Solution:
Il est possible, si vous ajoutez:
protected $with = ['company'];
aux modèles Service
et Product
. De cette façon, la relation company
est chargée à chaque fois qu'un Service
ou un Product
est chargé, y compris lorsqu'il est chargé via la relation polymorphe avec History
.
Explication:
Cela se traduira par 2 requêtes supplémentaires, une pour Service
et une pour Product
, soit une requête pour chaque historable_type
. Votre nombre total de requêtes, quel que soit le nombre de résultats n
, va donc de m+1
(sans charger avec ardeur la relation company
distante) à (m*2)+1
, où m
est le nombre de modèles liés par votre relation polymorphe.
Facultatif:
L'inconvénient de cette approche est que vous toujours chargez avec impatience la relation company
sur les Service
et Product
. Cela peut ou non être un problème, selon la nature de vos données. Si c'est un problème, vous pouvez utiliser cette astuce pour charger automatiquement company
automatiquement lors de l'appel de la relation polymorphe.
Ajoutez ceci à votre modèle History
:
public function getHistorableTypeAttribute($value)
{
if (is_null($value)) return ($value);
return ($value.'WithCompany');
}
Désormais, lorsque vous chargez la relation polymorphe historable
, Eloquent recherche les classes ServiceWithCompany
et ProductWithCompany
, plutôt que Service
ou Product
. Ensuite, créez ces classes et placez-y 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'];
}
... et enfin, vous pouvez supprimer protected $with = ['company'];
à partir des classes de base Service
et Product
.
Un peu hacky, mais ça devrait marcher.
Vous pouvez séparer la collection, puis charger paresseusement chaque:
$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;
Ce n'est probablement pas la meilleure solution, en ajoutant protected $with = ['company'];
comme suggéré par @damiani est une bonne solution, mais cela dépend de la logique de votre entreprise.
Comme João Guilherme l'a mentionné, cela a été corrigé dans la version 5.3 Cependant, je me suis retrouvé confronté au même bogue dans une application où il n'est pas possible de mettre à niveau. J'ai donc créé une méthode de remplacement qui appliquera le correctif aux API héritées. (Merci João, de m'avoir indiqué dans la bonne direction pour produire ceci.)
Créez d'abord votre classe 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();
}
}
Ensuite, vous aurez besoin de quelque chose qui permette à vos classes de modèles de parler à votre incarnation de MorphTo plutôt qu'à Eloquent. Cela peut être fait soit par un trait appliqué à chaque modèle, soit par un enfant de Illuminate\Database\Eloquent\Model qui est étendu par vos classes de modèle au lieu de Illuminate\Database\Eloquent\Model directement. J'ai choisi d'en faire un trait. Mais au cas où vous choisiriez d'en faire une classe enfant, j'ai laissé dans la partie où il infère le nom en tête-à-tête que c'est quelque chose que vous devez considérer:
<?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);
}
}
Je ne suis pas sûr à 100% à ce sujet, car il est difficile de recréer votre code dans mon système, mais peut-être que belongTo('Company')
devrait être morphedByMany('Company')
. Vous pouvez également essayer morphToMany
. J'ai pu obtenir une relation polymorphe complexe à charger correctement sans appels multiples. ?