Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel - Eager Loading Polymorphic Relation's Related Models

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

history table

companies

enter image description here

products

enter image description here

services

enter image description here

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?

like image 560
Wonka Avatar asked Nov 04 '14 03:11

Wonka


People also ask

What is difference between eager and lazy loading Laravel?

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.

How does Laravel eager loading work?

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.

What is polymorphic relationship in Laravel?

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.

What is the benefit of eager loading when do you use it Laravel?

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.


2 Answers

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.

like image 116
damiani Avatar answered Sep 22 '22 01:09

damiani


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.

like image 24
Razor Avatar answered Sep 21 '22 01:09

Razor