Jonathon
Jonathon

Reputation: 16283

Laravel 5: Lazy Eager Loading without repeating queries

In my project I'm writing a method on one of my models that uses one of it's relationships and a sub-relationship so I am having to use lazy eager loading:

class MyModel extends Model
{
    public function doSomething()
    {
        // Lazy eager load relationship and subrelationship
        $this->load('relationship', 'relationship.subrelationship');

        $relatedItems = $this->relationship;
        foreach ($relatedItems as $relatedItem) {
            $subrelatedItems = $relatedItem->subrelationship;
            foreach ($subrelatedItems as $subrelatedItem) {
                // Do something...
                return true;
            }
        }

        return false;
    }
}

The Model::load() method in Laravel can be used to re-load a relationship and does a new database query every time. Therefore, each time I call my method MyModel::doSomething(), (Or call another method that uses similar relationships) another database query is executed.

I know that in Laravel you can call a relationship a number of times like this:

$relatedItems = $model->relationship;
$relatedItems = $model->relationship;
$relatedItems = $model->relationship;
$relatedItems = $model->relationship;

..and it doesn't repeat the query since it has already loaded the relationship.

I was wondering if it was possible to avoid querying the database every time I want to use my relationship inside the model? I had the idea that I could use $this->getRelations() to work out which relationships had been loaded and then just skip them if they already had:

$toLoad = ['relationship', 'relationship.subrelationship'];
$relations = $this->getRelations();
foreach ($toLoad as $relationship) {
    if (array_key_exists($relationship, $relations)) {
        unset($toLoad[$relationship]);
    }
}

if (count($toLoad) > 0) {
    $this->load($toLoad);
}

That works to an extent, it's able to skip loading relationship each time, but relationship.subrelationship isn't actually stored in the array returned by $this->getRelations(). I would imagine it is stored in the sub-model(s) as subrelationship.

Cheers

Upvotes: 1

Views: 1829

Answers (2)

Jonathon
Jonathon

Reputation: 16283

I've managed to work out a way around this problem. Originally I had something like this:

class MyModel extends Model
{
    public function relationship()
    {
        return $this->hasMany('App\Related');
    }
}

class Related extends Model
{
    public function subrelated()
    {
        return $this->belongsTo('App\Subrelated');
    }
}

class Subrelated extends Model
{

}

After a lot of digging round in the Laravel source, I have found that when you call a relationship like it's a property using a magic method (namely __get()), Laravel stores it in the models $relations property for use later. With that in mind I added another method to MyModel like this:

class MyModel extends Model
{
    public function relationship()
    {
        return $this->hasMany('App\Related');
    }

    public function relationshipWithSubrelated()
    {
        return $this->relationship()->with('subrelated');
    }
}

Now I can call the relationship like the following as many times as I need to and it always gives me the same results:

$myModel = MyModel::find(1);
$myModel->relationshipWithSubrelated;

Wish I thought of it before I spent hours trying to work around!

Upvotes: 1

Mihkel Allorg
Mihkel Allorg

Reputation: 989

I think the problem here is, that you're creating the function in the Model file. And that kind of limits you and you have to load the relationship every time.

What I would do is create a function which takes the model objects you want to doSomething with as arguments. Let me clarify:

Define a function

Now if you're following a design pattern you probably have a place to put the function in. Otherwise put it where you'd normally put it.

public function doSomething($myModels)
{
    $relatedItems = $myModels->relationship;
    foreach ($relatedItems as $relatedItem) {
        $subrelatedItems = $relatedItem->subrelationship;
        foreach ($subrelatedItems as $subrelatedItem) {
            // Do something...
            return true;
        }
    }

    return false;
}

Now when calling the function, pass the models with the relationship.

For example

$myModels = MyModel::where('created_at', <, Carbon::now())->with('relationship')->get();
doSomething($myModels)

If you need to load several or deeper relationships you can do

...->with('relationship.subrelationship', 'secondrelationship')->get()

The code isn't tested but I think you get the point. Look into with()

Upvotes: 1

Related Questions