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
and Service
.
I’ve been working on this for days but can't find a solution.
History::with('historable.company')->get()
just ignores the company
in historable.company
.
What would an efficient solution to this problem be?
You can separate the collection, then lazy eager load each one:
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.I'm not 100% sure on this, because it's hard to re-create your code in my system but perhaps
belongTo('Company')
should bemorphedByMany('Company')
. You could also trymorphToMany
. I was able to get a complex polymorphic relationship to load properly without multiple calls. ?Solution:
It is possible, if you add:
to both the
Service
andProduct
models. That way, thecompany
relation is eager-loaded every time aService
or aProduct
is loaded, including when loaded via the polymorphic relation withHistory
.Explanation:
This will result in an additional 2 queries, one for
Service
and one forProduct
, i.e. one query for eachhistorable_type
. So your total number of queries—regardless of the number of resultsn
—goes fromm+1
(without eager-loading the distantcompany
relation) to(m*2)+1
, wherem
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 theService
andProduct
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-loadcompany
only when calling the polymorphic relation.Add this to your
History
model:Now, when you load the
historable
polymorphic relation, Eloquent will look for the classesServiceWithCompany
andProductWithCompany
, rather thanService
orProduct
. Then, create those classes, and setwith
inside them:ProductWithCompany.php
ServiceWithCompany.php
...and finally, you can remove
protected $with = ['company'];
from the baseService
andProduct
classes.A bit hacky, but it should work.
Pull Request #13737 and #13741 fixed this issue.
Just update your Laravel version and the following code
Will work as expected.
As João Guilherme mentioned, this was fixed in version 5.3 However, I've found myself facing the same bug in an App where it's not feasible to upgrade. So I've created an override method that will apply the fix to Legacy APIs. (Thanks João, for pointing me in the right direction to produce this.)
First, create your Override class:
Next, you'll need something that lets your Model classes actually talk to your incarnation of MorphTo rather than Eloquent's. This can be done by either a trait applied to each model, or a child of Illuminate\Database\Eloquent\Model that gets extended by your model classes instead of Illuminate\Database\Eloquent\Model directly. I chose to make this into a trait. But in case you chose to make it a child class, I've left in the part where it infers the name as a heads-up that that's something you'd need to consider: