Laravel - getting relation in loop - best practices - php

Just an example:
let's say I have Post model, and the Comment model. Post, of course, have Comments, one-to-many relation.
I have to display list of posts with comments below it.
I'll get my posts in the controller:
$posts = Post::get(), I'll pass it to the blade view and then I'll loop through it
#foreach($posts as $post)
{{ $post->title }}
{{ $post->comments }}
#endforeach
where $post->comments is some relation
public function comments()
{
return $this->hasMany(Comment::class);
}
As we know, that query will be executed many times.
Now my question: how we should optimize it?
Return Cache::remember in the getter?
Get (somehow?) those comments, when getting the posts in one query? Something like join query? I know that I can write that kind of query, but I'm talking about Eloquent's query builder. And then how get the comments within the loop? Wouldn't {{ $post->comments }} call the relation again instead of getting stored data?
Different solution?

You can do $posts = Post::with('comments')->get() to eager load the comments with the post. Read more about it in the documentation: https://laravel.com/docs/5.7/eloquent-relationships#eager-loading
Also, to display the comments you would want to add another foreach loop. It would look something like this:
#foreach($posts as $post)
{{ $post->title }}
#foreach($post->comments as $comment)
{{ $comment->title }}
#endforeach
#endforeach

You’ve probably cached some model data in the controller before, but I am going to show you a Laravel model caching technique that’s a little more granular using Active Record models
Note that we could also use the Cache::rememberForever() method and rely on our caching mechanism’s garbage collection to remove stale keys. I’ve set a timer so that the cache will be hit most of the time, with a fresh cache every fifteen minutes.
The cacheKey() method needs to make the model unique, and invalidate the cache when the model is updated. Here’s my cacheKey implementation:
public function cacheKey()
{
return sprintf(
"%s/%s-%s",
$this->getTable(),
$this->getKey(),
$this->updated_at->timestamp
);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function getCachedCommentsCountAttribute()
{
return Cache::remember($this->cacheKey() . ':comments_count', 15, function () {
return $this->comments->count();
});
}

yes u can do like that in controller
$minutes = 60;
$posts = Cache::remember('posts', $minutes, function () {
return Post::with('comments')->get()
});
in blade u can get like that
#foreach($posts as $post)
{{ $post->title }}
#foreach($post->comments as $comment)
{{ $comment->title }}
#endforeach
#endforeach
for more information read this article

Related

Laravel two queries, how do one query?

I have model post. I use this package: https://github.com/cyrildewit/eloquent-viewable
I have accesor in model post:
protected $appends = ['views'];
public function getViewsAttribute()
{
return $this->views()->count();
}
In blade when I foreach my posts:
#foreach($posts as $post)
Views: {{ $post->views }} {{ trans_choice('trans.views', $post->views)
#endforeach
I get two queries with views. And if posts 100, then queries will be 200.. For each post I get two same queries. How I can resolve this? If I delete {{ trans_choice('trans.views', $post->views) Then I get one query.
Given that views is a Laravel relationship, you can use the withCount function to "eager load" the value.
$posts = Post::withCount('views')->get();

Laravel #foreach

I need to get each comment of news . Its working good for firstorFail() item
{{$news->comments()->firstOrFail()->name}}
But bring me empty result when im try this one with foreach:
#foreach($news->comments() as $comment)
{{$comment->name}}
#endforeach
The function firstOrFail will return exactly one comment. Seems like you actually want all comments? You should use this in your controller
$news->comments; // yeah that's it, it will load all comments
Also return news
return view('my.view', compact('news'));
Then use it in blade
#foreach($news->comments as $comment)
{{$comment->name}}
#endforeach
the line
$news->comments()
would require you to also call ->get() , because comments() will return the relation instead of the actual data.
This: $news->comments() bringing you query builder probably. So what you need is to ->get() for execute query like this:
#foreach($news->comments()->get() as $comment)
{{$comment->name}}
#endforeach
Edit
or as a #Roy Philips mentioned: $news->comments

Laravel - Eager load a non-relationship method (N+1 issue)

With laravel's eager loading, I can do something like
$forums = Forum::with('posts', 'threads')->get();
To get the threads as its posts and forum without doing many queries.
However, I have a method that gets the last post of the forum.
This is my method in Forum.php
public function lastThread()
{
$forums = $this->arrayOfIDs(); // get array of all ids, even subforum's subforums.
return Thread::whereIn('forum_id', $forums)->orderBy('updated_at', 'desc')->first();
}
This is my partial view to get the last thread:
#if ($subforum->lastThread())
<a href="{{ viewThread($subforum->lastThread()) }}">
{{ $subforum->lastThread()->title }}
</a>
<br>
<a href="{{ viewPost($subforum->lastThread()->lastPost) }}"> {{ trans('forum/post.last_post') }}
:</a>
{{ $subforum->lastThread()->lastPost->user->info() }}
<br>
{{ $subforum->lastThread()->lastPost->created_at }}
#endif
As you can see, if I am doing $subforum->lastThread() a lot. I can store it in a variable like $lastThread = $subforum->lastThread() and then do something like $lastThread->created_at to reduce the number of queries but I figured it would be much easier if just include it with the original query to get all forums.
Something like:$forums = Forum::with('posts', 'threads', 'lastThread')->get(); perhaps? I have tried doing a hasOne relation for this method but that does not work because the last thread does not necessarily belong to that specific subforum.
Anyway, how can I best fix this N+1 problem?
Thank you.
you have endless options,
if the Cache class is right for you than I will go with this path, you can also do this:
protectd $lastThread;
public function lastThread()
{
if ($this->lastThread) return $this->lastThread;
return $this->fetchLastThread();
}
public function fetchLastThread() {
$forums = $this->arrayOfIDs(); // get array of all ids, even subforum's subforums.
$this->lastThread = Thread::whereIn('forum_id', $forums)->orderBy('updated_at', 'desc')->first();
return $this->lastThread;
}

How to manipulate an object in laravel

I have a laravel query as the one below:
$campaign = Campaign::with(array('tracks.flights' => function($q) use ($dates)
{
$q->whereRaw("flights.start_date BETWEEN '". $dates['start']."' AND '".$dates['end']."'")->orderBy('start_date')
->with('asset')
->with('comments');
}
))
->with('tracks.group')
->with('tracks.media')
->orderBy('created_at', 'desc')
->find($id);
I am very new to laravel and right now the response from this query returns all the data required including the comments with the comments attributes from the DB.
What i want to achieve is manipulate the comments object so it includes the user name from the users table as the comments table has the user_id only as an attribute.
How can I achieve this? I am very new to laravel.
Your Comment model must have a relationship with the User model, such as:
public function user()
{
return $this->belongsTo('App\User');
}
Now when you are iterating comments you are able to do $comment->user->name or in your case it will be something like this:
#if(!$campaign->tracks->flights->isEmpty())
#foreach($campaign->tracks->flights as $flight)
#if(!$flight->comments->isEmpty())
#foreach($flight->comments as $comment)
{!! $comment->user->name !!}
#endforeach
#endif
#endforeach
#endif
Once you can do this, next step is to understand eager load with eloquent.

How to show related items with pagination in Eloquent and Blade?

AdminsController
public function index(){
$someMessages=$this->blog->paginate(5);
$users=User::all();
// return 'Welcome Admin';
return View::make('admin.admin',['someMessages'=>$someMessages,'users'=>$users]);
}
admin.blade.php
Here need to display username from 'users' variable based on uid stored in 'someMessages' object.
#extends('layout.default')
#section('title')
<title>Welcome Admin</title>
#stop
#section('content')
#foreach($someMessages as $message)
<blockquote>{{$message['blog']}}
<small>
<cite>
</cite>
</small>
</blockquote>
#endforeach
{{ $someMessages->links()}}
#stop
If you have a Eloquent relation between blog messages and users defined correctly you don't need to query the users separately. What you want to do is just go with the eager loading:
//in case you call it author in your "relation definition"
$someMessages = $this->blog->with('author')->paginate(5);
And then in your Blade template:
#foreach($someMessages as $message)
<div>
<div>{{ $message->blog }}</div>
<div>{{ $message->author->name }}</div>
</div>
#endforeach
If you haven't declared the relationship yet - it's easy. Open up your user eloquent model class and add a method:
public function blogs()
{
return $this->hasMany('Blog', 'uid');
}
In the blog eloquent model class you'd add:
public function author()
{
return $this->belongsTo('User', 'uid');
}
More about eloquent and relationships can be found here.
If that's not the case you can still filter your users collection to get the one you want:
#foreach($someMessages as $message)
<?php
$user = $users->filter(function($user) use ($message)
{
return $user->id == $message->uid;
})->first();
?>
<div>
<div>{{ $message->blog }}</div>
<div>{{ $user->name }}</div>
</div>
#foreach
What happens here is you filter your users collection based on $message->uid value and take the first one from it (it should always be just one or none since user IDs are unique).
Although you need to understand that in this case you will get all the users from database and filter through them for every blog message you're outputting. Eager loading is a much better idea here and I'd stick to it if possible.

Categories