Laravel : Can appends attributes lead to a n+1 problem?

Viewed 1368

Introduction
I start using appends attributes within models during fall 2019. At start, it was simply to run function that will join two column, but now, I use them to make some analysis on certain models before sending it to the frontend. Now, I'm suspecting this to lead to a n+1 problem. So, let check on some use of the appends attributes.

Basic usage

<?php

namespace App;

class User extends Authenticatable
{
    protected $dates = ['deleted_at'];
    protected $table = 'users';

    protected $appends =
    [
        'full_name',
    ];

    protected $fillable =
    [
        'username',
        'first_name',
        'last_name',
    ];

    getFullNameAttribute()
    {
        return $this->first_name.' '.$this->last_name;
    }
}

So, that let you append the full name, we know that, but under the hood?

$user = User::find($id);

Using appends with the above model retrieve is almost incensitive for any application. If we push it a little bit, what really happen?

$users = User::get();

Now, is the query to the database is the same as if we don't append anything? In other word, is the appending happen during the query to DB or after? Whatever, you need the full name and if it's not in the Database or the server, you will do it in the frontend, so that need to happen anyway. Which of the three is the fastest is for another question.

A bit more complexe usage

namespace App;

class User extends Authenticatable
{
    protected $dates = ['deleted_at'];
    protected $table = 'users';

    protected $appends =
    [
        'full_name',
        'last_post',
    ];

    protected $fillable =
    [
        'username',
        'first_name',
        'last_name',
    ];

    protected $hidden =
    [
        'password',
        'remember_token',
    ];

    public function posts()
    {
        return $this->hasMany('App\Post', 'user_id');
    }

    getFullNameAttribute()
    {
        return $this->first_name.' '.$this->last_name;
    }

    getLastPostAttribute()
    {
        return $this->posts()->last()
    }
}

Now it's obvious that if I do $users->get(); it will result into a n+1 problem. But if I do this $users->with('posts')->get(); Do I have the n+1 problem or worst?

If your not use with n+1 problem
The n+1 problem is when you create a query that will need to make the initial call to DB and one call for each object into the array. $users->get() will get all users and then, since I append the last_post, since post where not retrieved, it will make a DB call for each of the models.

What I want to know, it's in the second case $users->with('posts')->get(), when Laravel will append last_post, will it know that it already have all the posts? So it doesnt need to load them a second time for each users?

2 Answers

So this attribute will cause n+1 queries no matter what you preload

getLastPostAttribute()
{
    return $this->posts()->last()
}

By accessing posts() as a method you are creating a new query builder instance.

Replace it with return $this->posts->last() and then you can preload you posts relationship and avoid your extra queries

If you are unsure if you are getting n+1 issues you can require this great package from beyondcode beyondcode/laravel-query-detector which will inform you of any n+1's you have.

https://github.com/beyondcode/laravel-query-detector

To fix your n+1...


$users = Users::with(['posts'])->get();


public function getFullNameAttribute()
{
    return $this->first_name.' '.$this->last_name;
}

public function getLastPostAttribute()
{
    return $this->posts->last(); //update to not use builder instance
}
Related