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

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?

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

Laravel 9 - specular model relations not working

i can't figure out why my eloquent relationship is not working.
I have the grid and details tables related (es. grid.id = details.order_id). This works for first 2 tables but not for other 2 that are almost a copy of the first.
Working relationship code :
OrderGrid model:
use HasFactory;
protected $table = 'orders_grid';
protected $fillable = [
'user_id',
// more data
];
public function order_detail()
{
return $this->hasMany('App\Models\OrderDetail');
}
OrderDetail model:
use HasFactory;
protected $table = 'orders_detail';
protected $fillable = [
'order_id',
// more data
];
public function order_grid()
{
return $this->belongsTo('App\Models\OrderGrid', 'order_id', 'id');
}
In controller
$data_details = OrderDetail::where('order_id', $id)->get();
in view
$data_details->first()->order_grid->id
// so i can get the ID from the related table
Now i have two more very similar models which relations are not working:
OrderGrid model 2
use HasFactory;
protected $table = 'orders_grid_jde';
protected $fillable = [
'total_order',
// more data
];
public function detail_jde()
{
return $this->hasMany('App\Models\OrderDetailJde');
}
OrderDetail model 2 :
use HasFactory;
protected $table = 'orders_detail_jde';
protected $fillable = [
'order_jde_id',
];
public function grid_jde()
{
return $this->belongsTo('App\Models\OrderGridJde', 'order_jde_id', 'id');
}
In controller:
$data_details = OrderDetailJde::where('order_jde_id', $id)->get();
In view:
$data_details->first()->order_jde_id->id
This is not working, if i dump($data_details->first()) relations is empty array, not as in the screen shown before
Another weird behaviour on working relation: if i dump $data_details->first() in the controller i don't see the relation, but if i do it in view it shows as in the screen.
Sorry for the long post, tried to be understandble as possible
In your Controller call the relation like this:
$data_details = OrderDetailJde::where('order_jde_id', $id)->with("grid_jde")->get();
In view:
$data_details->first()
It will give all the detils.
It should work now.
In the view where you have the error, you are accessing the directly to the field named order_jde_id instead of the relationship grid_jde.
Tips:
You should eager load the relationships if you are planning to use these in the view.
$books = Book::with('author')->get();
https://laravel.com/docs/9.x/eloquent-relationships#eager-loading

Connection protected when I want to see my item in detail Laravel

I am on laravel 9, I have my database with my element that I want to see in detail but it shows me this when I want to see it, (I see it when I want to display all the elements)
error message
Controller
public function show(Tickets $tickets)
{
var_dump($tickets);
return view('tickets.show', ['ticket' => $tickets]);
}
Models
class Tickets extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'title',
'description',
'status',
'priority',
'type',
'assigned_to',
];
}
Routes
Route::get('oauth', [gCalendarController::class, 'oauth']);
I think you returned the object of Tickets Model like #lagbox commented.
If you want to get all ticket when accessing to show() function, you need to do like this:
public function show(Tickets $tickets)
{
// Get all the ticket
$ticket = $tickets->all();
return view('tickets.show', compact('ticket'));
}
Hope this help.

Laravel Relationship Without Timestamps

Is there away to call a model with a relationship without timestamps when calling the with statement in the model itself - instead of going into that relationship model and saying $protected timestamps = false;
e.g
class Post Extends Eloquent {
public function template() {
return $this->hasOne('App\Template', 'id', 'template_id')->timestamps(false);
}
}
You can add this in following model to skip the values
protected $hidden = [
'created_at',
'updated_at'
];

Automatic eager loading?

Rather than doing something like this (which I do dozens of times across the site):
$posts = Post::with('user')
->with('image')
->get();
Is it possible to automatically call with('image') whenever with('user') is called? So in the end, I could do just:
$posts = Post::with('user')
->get();
And still eager load image?
Add the following in your model:
protected $with = array('image');
and that should do the trick.
The $with attribute lists relations that should be eagerly loaded with every query.
Here another solution that works like a charm !
class Post extends Model {
protected $table = 'posts';
protected $fillable = [ ... ];
protected $hidden = array('created_at','updated_at');
public function user()
{
return $this->belongsTo('App\Models\User');
}
public function userImage()
{
return $this->belongsTo('App\Models\User')->with('image');
}
}
$posts = Post::with('userImage')->get();
Using this you can still use your user posts $posts = Post::with('user')->get(); whenever you don't want to make an additional call to retrieve images ..

How do I prevent fetching data multiple times inside a Repository?

My EloquentUserRepository (concrete implementation) has some methods like getCompanies($userId) and getProfile($userId), for example.
In both cases, before returning stuff, they fetch the user, like so:
$user = User::find($userId);
So, when I need to call those two methods in the same request, the data is fetched twice.
Am I missing something or am I using repositories wrongly?
What if you approached it from the relationship side instead? I'm assuming that Companies live in a different table. So could you acquire the companies with something like this?
<?php
class User extends Model {
protected $table = 'users';
protected $fillable = ['name', 'email', 'avatar'];
protected $hidden = ['password', 'remember_token'];
public function companies()
{
return $this->hasMany('App\Company', 'user_id');
}
}
Then you could find the companies with:
$user = User::find($userId)->load('companies')->get();
and you'd already have the companies loaded.
If the profile is in a different table you could do a similar relationship, like this:
<?php
class User extends Model {
protected $table = 'users';
protected $fillable = ['name', 'email', 'avatar'];
protected $hidden = ['password', 'remember_token'];
public function profile()
{
return $this->hasOne('App\Profile', 'user_id');
}
}
I'm not certain if this is exactly what you are looking for, but it's the approach I like to use when finding items that are related to a particular model.

Resources