Laravel - Désireux de Chargement Polymorphe de la Relation d'Modèles associés
je peux désireux de charge polymorphes relations/modèles sans n+1 points. Cependant, si j'essaie d'accéder à un modèle lié au modèle polymorphique, le problème n+1 apparaît et je n'arrive pas à trouver de solution. Voici la configuration exacte pour la voir localement:
1) Tableau DB nom/
history
companies
products
services
2) des 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');
}
}
3) Routage
Route::get('/history', function(){
$histories = History::with('historable')->get();
return View::make('historyTemplate', compact('histories'));
});
4) Modèle avec n+1 enregistré uniquement becacuse de $de l'histoire->historable->->nom, commentaire, n+1 s'en va.. mais nous avons besoin de ce nom de compagnie apparenté à distance:
@foreach($histories as $history)
<p>
<u>{{ $history->historable->company->name }}</u>
{{ $history->historable->name }}: {{ $history->historable->status }}
</p>
@endforeach
{{ dd(DB::getQueryLog()); }}
j'ai besoin de pouvoir charger les noms d'entreprises avec impatience (dans une seule requête) car c'est un modèle apparenté des modèles de relation polymorphique 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
historable.company
.
Ce serait une solution efficace à ce problème?
5 réponses
Solution:
Il est possible, si vous ajoutez:
protected $with = ['company'];
à la fois à l' Service
et Product
modèles. De cette façon, le company
relation est impatient-chargé à chaque fois un Service
et Product
est chargé, y compris lorsqu'il est chargé via la relation polymorphique avec History
.
Explication:
il en résultera 2 requêtes supplémentaires, dont une pour Service
et un Product
, i.e. une requête pour chaque historable_type
. Si votre nombre total de requêtes-quel que soit le nombre de résultats n
-va de m+1
(sans avoir à charger le distant company
à (m*2)+1
, où m
est le nombre de modèles liés par votre polymorphes relation.
en Option:
L'inconvénient de cette approche est que vous avez toujours désireux de charge company
rapport sur l' Service
et Product
modèle. 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 automatiquement désireux de charge company
lors de l'appel de la relation polymorphique.
ajoutez ceci à votre History
modèle:
public function getHistorableTypeAttribute($value)
{
if (is_null($value)) return ($value);
return ($value.'WithCompany');
}
maintenant, quand vous chargez le historable
polymorphes égard, Éloquent va chercher les classes ServiceWithCompany
et ProductWithCompany
plutôt que Service
ou Product
. Ensuite, créez ces classes, et mettez with
à l'intérieur d'eux:
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 de la base Service
et Product
classes.
un peu hacky, mais ça devrait marcher.
Vous pouvez séparer la collection, puis paresseux impatient charger chacun:
$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;
probablement ce n'est pas la meilleure solution, ajoutant protected $with = ['company'];
comme suggéré par @damiani est une bonne solution, mais cela dépend de votre logique commerciale.
Pull Request #13737 et n ° 13741 correction de ce problème.
il suffit de mettre à jour votre version de Laravel et le code suivant
protected $with = [‘likeable.owner’];
fonctionnera comme prévu.
Je ne suis pas sûr à 100% de cela, parce qu'il est difficile de recréer votre code dans mon système mais peut-être belongTo('Company')
doit être morphedByMany('Company')
. Vous pouvez également essayer morphToMany
. J'ai pu obtenir une relation polymorphe complexe à charger correctement sans appels multiples. ?
tout d'abord, créez 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 permet à vos classes de Model de parler réellement à votre incarnation de MorphTo plutôt que celui D'Eloquent. Cela peut être fait soit par un trait appliqué à chaque modèle, soit par un enfant de Illuminate\Database\Eloquent\Model qui est prolongé par vos classes de model au lieu d'Illuminate\Database\Eloquent\Model directement. J'ai choisi de faire cela en un trait. Mais dans le cas où vous choisiriez d'en faire un cours pour enfants, j'ai laissé dans la partie où le nom apparaît comme une mise en garde 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);
}
}