Подтвердить что ты не робот

Laravel - нетерпеливые модели, связанные с полиморфизмом

Я могу нетерпеливо переносить полиморфные отношения/модели без каких-либо проблем n + 1. Однако, если я попытаюсь получить доступ к модели, связанной с полиморфной моделью, появится проблема n + 1, и я не могу найти исправления. Вот точная установка, чтобы увидеть ее локально:

1) Имя/данные таблицы таблиц

history

history table

  companies

enter image description here

  products

enter image description here

  services

enter image description here

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');
    }
}

3) Маршрутизация

Route::get('/history', function(){
    $histories = History::with('historable')->get();
    return View::make('historyTemplate', compact('histories'));
});

4) Шаблон с n + 1 занесен в журнал только из истории history- > historyable- > company- > , прокомментируйте это, 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. Я работаю над этим в течение нескольких дней, но не могу найти решение. History::with('historable.company')->get() просто игнорирует company в historable.company. Каким будет эффективное решение этой проблемы?

4b9b3361

Ответ 1

Решение:

Возможно, если вы добавите:

protected $with = ['company']; 

для моделей Service и Product. Таким образом, отношение company загружается каждый раз при загрузке Service или Product, в том числе при загрузке через полиморфное отношение с History.


Объяснение:

Это приведет к дополнительным 2 запросам, одному для Service и одному для Product, то есть одному запросу для каждого historable_type. Таким образом, ваше общее количество запросов - независимо от количества результатов n - от m+1 (без привязки к далекому отношению company) до (m*2)+1, где m - количество моделей, связанных ваше полиморфное отношение.


Дополнительно:

Недостатком этого подхода является то, что вы всегда будете загружать отношение company в моделях Service и Product. Это может быть или не быть проблемой, в зависимости от характера ваших данных. Если это проблема, вы можете использовать этот трюк, чтобы автоматически загружать company только при вызове полиморфного отношения.

Добавьте это в свою модель 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'];
}

... и, наконец, вы можете удалить protected $with = ['company']; из базовых классов Service и Product.

Немного взломанный, но он должен работать.

Ответ 2

Вы можете отделить коллекцию, а затем ленивую загрузку каждого из них:

$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, это хорошее решение, но оно зависит от вашей бизнес-логики.

Ответ 3

Запрос Pull # 13737 и # 13741 зафиксировал это проблема.

Просто обновите версию Laravel и следующий код

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

Будет работать как ожидалось.

Ответ 4

Я не уверен на 100%, потому что трудно создать код в моей системе, но, возможно, belongTo('Company') должен быть morphedByMany('Company'). Вы также можете попробовать morphToMany. Я смог получить сложную полиморфную связь для правильной загрузки без нескольких вызовов.

Ответ 5

Как отметил João Guilherme, это было исправлено в версии 5.3. Однако я обнаружил, что сталкиваюсь с такой же ошибкой в ​​приложении, где это невозможно выполнить. Поэтому я создал метод переопределения, который применит исправление к API Legacy. (Спасибо, Жоао, за то, что указал мне в правильном направлении, чтобы произвести это.)

Сначала создайте класс 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();
    }
}

Затем вам понадобится что-то, что позволяет вашим классам моделей говорить с вашим воплощением MorphTo, а не с Eloquent. Это может быть сделано либо чертой, применяемой к каждой модели, либо дочерним элементом 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 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);
    }
}