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
.
Каким будет эффективное решение этой проблемы?
Ответы
Ответ 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);
}
}