Laravel - Eager Loading Polymorphic Relation's Related Models
Solution 1
Solution:
It is possible, if you add:
protected $with = ['company'];
to both the Service
and Product
models. That way, the company
relation is eager-loaded every time a Service
or a Product
is loaded, including when loaded via the polymorphic relation with History
.
Explanation:
This will result in an additional 2 queries, one for Service
and one for Product
, i.e. one query for each historable_type
. So your total number of queries—regardless of the number of results n
—goes from m+1
(without eager-loading the distant company
relation) to (m*2)+1
, where m
is the number of models linked by your polymorphic relation.
Optional:
The downside of this approach is that you will always eager-load the company
relation on the Service
and Product
models. This may or may not be an issue, depending on the nature of your data. If this is a problem, you could use this trick to automatically eager-load company
only when calling the polymorphic relation.
Add this to your History
model:
public function getHistorableTypeAttribute($value)
{
if (is_null($value)) return ($value);
return ($value.'WithCompany');
}
Now, when you load the historable
polymorphic relation, Eloquent will look for the classes ServiceWithCompany
and ProductWithCompany
, rather than Service
or Product
. Then, create those classes, and set with
inside them:
ProductWithCompany.php
class ProductWithCompany extends Product {
protected $table = 'products';
protected $with = ['company'];
}
ServiceWithCompany.php
class ServiceWithCompany extends Service {
protected $table = 'services';
protected $with = ['company'];
}
...and finally, you can remove protected $with = ['company'];
from the base Service
and Product
classes.
A bit hacky, but it should work.
Solution 2
You can separate the collection, then lazy eager load each one:
$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;
Probably it's not the best solution, adding protected $with = ['company'];
as suggested by @damiani is as good solution, but it depends on your business logic.
Solution 3
Pull Request #13737 and #13741 fixed this issue.
Just update your Laravel version and the following code
protected $with = [‘likeable.owner’];
Will work as expected.
Wonka
Updated on March 01, 2020Comments
-
Wonka about 4 years
I can eager load polymorphic relations/models without any n+1 issues. However, if I try to access a model related to the polymorphic model, the n+1 problem appears and I can't seem to find a fix. Here is the exact setup to see it locally:
1) DB table name/data
history
companies
products
services
2) Models
// 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) Routing
Route::get('/history', function(){ $histories = History::with('historable')->get(); return View::make('historyTemplate', compact('histories')); });
4) Template with n+1 logged only becacuse of $history->historable->company->name, comment it out, n+1 goes away.. but we need that distant related company name:
@foreach($histories as $history) <p> <u>{{ $history->historable->company->name }}</u> {{ $history->historable->name }}: {{ $history->historable->status }} </p> @endforeach {{ dd(DB::getQueryLog()); }}
I need to be able to load the company names eagerly (in a single query) as it's a related model of the polymorphic relation models
Product
andService
. I’ve been working on this for days but can't find a solution.History::with('historable.company')->get()
just ignores thecompany
inhistorable.company
. What would an efficient solution to this problem be?