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?
Lazy Loading vs. Eager Loading. While lazy loading delays the initialization of a resource, eager loading initializes or loads a resource as soon as the code is executed. Eager loading also involves pre-loading related entities referenced by a resource.
Eager loading is a concept in which when retrieving items, you get all the needed items together with all (or most) related items at the same time. This is in contrast to lazy loading where you only get one item at one go and then retrieve related items only when needed.
A one-to-one polymorphic relationship is a situation where one model can belong to more than one type of model but on only one association. A typical example of this is featured images on a post and an avatar for a user. The only thing that changes however is how we get the associated model by using morphOne instead.
It makes sense to eager-load if your logic branches where those additional relations are needed on one path, but not on another; in either case the parent model(s) are needed and perhaps already fetched in that request. Eager Loading: Eager Loading helps you to load all your needed entities at once.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With