Laravel - Eager Loading Polymorphic Relation's Related Models

24,343

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.

Share:
24,343
Wonka
Author by

Wonka

Updated on March 01, 2020

Comments

  • Wonka
    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

    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?