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

history table



companies

enter image description here



products

enter image description here



services

enter image description here



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 companyhistorable.company. Ce serait une solution efficace à ce problème?

21
demandé sur matpop 2014-11-04 06:06:21

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.

45
répondu damiani 2014-11-09 16:09:00

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.

8
répondu Razor 2014-11-09 05:10:14

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.

3
répondu João Guilherme 2016-05-30 12:08:28

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

0
répondu dwenaus 2014-11-07 22:06:49

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);
    }
}
0
répondu Claymore 2017-02-28 19:38:15