web-dev-qa-db-fra.com

Laravel - Désireux de charger les modèles associés de la relation polymorphe

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

history table

companies

enter image description here

products

enter image description here

services

enter image description here

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?

28
Wonka

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.

58
damiani

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.

10
Razor

Pull Request # 13737 et # 13741 a résolu ce problème.

Mettez à jour votre version Laravel et le code suivant

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

Fonctionnera comme prévu.

4
João Guilherme

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);
    }
}
0
kmuenkel

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. ?

0
dwenaus