1

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

2 Answers 2

1

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()

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for your answer - I understand what you're saying. In my case, I think that the method is suitable for the model. Given a user object (By Auth::user() I'm trying to find out something about that user) so using with is quite difficult since the model is already loaded. I'm trying to achieve something similar to what with does in that I want to load my relationships and then just use them normally without running into the n+1 problem. Really appreciate your answer though - thanks!
How are you retrieving the model? Before calling ->get() or ->first() you can call ->with() and it's also loaded.
I was retrieving the model using Auth::user() and then I was calling the relationships like $this->relationship i.e. the magic method. I have tried $this->relationship()->with('subrelationship')->get() but that still means repeated queries since doing that doesn't store the relationship in the relations property. I have just figured a way around this problem though, if you bear with me a moment while I post an answer. Thanks a lot for your suggestions though.
1

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!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.