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

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;
}

Related

laravel eager loading for self relation

I have the model that has relation to itself like this
public function children() {
return $this->hasMany(AppointmentPart::class,'parent_id');
}
I want to eager loading for queries for this model but because every row can have child with() method doesn't work for me
$parts = AppointmentPart::whereParentId(0)->with('children')->get();
my blade:
<ul id="treeData">
#foreach($parts as $part)
<li id="{{ $part->id }}"
#if(!empty($part->children)) class="folder" #endif>
{{ $part->title }}
#if(!empty($part->children))
#include('appointment::admin.appointment.layout._part_child', array('parts' => $part->children))
#endif
</li>
#endforeach
</ul>
content of _part_child :
<ul>
#foreach($parts as $part)
<li id="{{ $part->id }}" #if(!empty($part->children)) class="folder" #endif>
{{ $part->title }}
#include('appointment::admin.appointment.layout._part_child',['parts'=>$part->children])
</li>
#endforeach
</ul>
how can I do this?
You should use a whereHas() function. Have a look at the official documentation.
If you need even more power, you may use the whereHas and orWhereHas methods to put "where" conditions on your has queries. These methods allow you to add customized constraints to a relationship constraint [...]
So in your case you have to change your method as follows:
$parts = AppointmentPart::with('children')->whereHas('children', function($query) use ($parent_id) {
$query->where('parent_id', '=', $parent_id);
})->get();
you can also create a local scope in order to create a reusable query statement.
This is an example for you. This might help to solve your problem. I have Structure Model which contain organizational structure of a company.
In my StructureController.php
public function index()
{
$parentStructures = Structure::where('parent_id', 0)->get();
return view('structure.structureTreeview', compact('parentStructures'));
}
In my Structure.php, I add with() at the relationship for eager loading.
public function children()
{
return $this->hasMany(Structure::class, 'parent_id')->with('children');
}
And, if you you check with this package here , it won't show any warning.
Reference : check this website here

Laravel - getting relation in loop - best practices

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

view cannot access variable defined in controller in laravel

I could not think how to write question for this problem.
So this is my problem.
I have two views
store-side-menu.blade.php
×
#foreach($store_categories as $cats)
{{ $cats->category_of_store }}
#endforeach
employ.blade.php
#section('side-menu-section')
#include('fc.static.store-side-menu')
#endsection
In employ.blade.php is only includes the store-side-menu.blade.php. I have designed this structure due to need.
In controller I have
public function employment()
{
$search = "";
$store_categoies = Category::all();
return view('fc.employ.employ', compact('search', 'store_categories'));
}
Now I have included store_categories in compact but store-side-menu.blade.php cannot access it. How do I achieve with its solution.
Try the code below and you should be good to go:
#section('side-menu-section')
#include('fc.static.store-side-menu', ['store_categories' => $store_categories])
#endsection

Trouble fetching data from 1 to 1 relationsip Laravel

I have set up two model with its row in table. And made a single form to fill both tables and it works perfectly
Tour.php
public function featuredImage()
{
return $this->hasOne('App\FeaturedImage');
}
tours table
id|name|content|featured_status
featuredImage.php
public function tour()
{
return $this->belongsTo('App\Tour');
}
Featured_images table
id|tour_id|path|name
Code in my controller to pass data to view.
$tours = Tour::where('featured', 1)->get();
return view('public.pages.index')
->withTours($tours);
Code in my view
#foreach($tours as $featured)
<div class="thumbnail">
<img src="{{$featured->featuredimage->path}}" alt="{{$featured->featuredImage->name}}">
</div>
<h4>{{$featured-name}}</h4>
#endforeach
The trouble is I'm not able to fetch featured images by writing
{{$featured->featuredimage->path}}
and the error is
Trying to get property of non-object
on the line {{$featured->featuredimage->path}}. I have used this method in my previous project and it had worked perfectly but it isn't going well in this one.
I tried replacing {{$featured->featuredimage->path}} with {{$featured->featuredImage->path}} but didn't worrked out.
Do this:
{{ $featured->featuredImage()->path }}
Also, you're creating a lot of additional queries here. You should use eager loading to solve N + 1 problem:
$tours = Tour::with('featuredImage')->where('featured', 1)->get();
And display data with:
{{ $featured->featuredImage->path }}

Displaying a checkbox only for the shows(in the tvshow table) that don't match the entries for a given user id in the watchedtvshow table - Laravel

This is what I want to do:
Display a checkbox for the tvshow field entry(from the tvshow table) only if that field entry doesn't match the entries for a given user id in the 'watchedtvshow' table
Table structure for 'tvshow':
id tvshow
Table structure for 'watchedtvshow'
id uid tvid(id of the tvshow)
Here is my controller method:
$tvshow = TVShow::with('watchedtvshow')->get();
return View::make('browse',['tvshow' => $tvshow]);
My View:
#foreach($tvshow as $show)
{{ $show->title }} {{ 'a checkbox' }}
#endforeach
What I tried:
In my controller method:
$tvshow = TVShow::with('watchedtvshow')->get();
$uid = NULL;
if(Auth::check())
$uid = Auth::user()->id;
return View::make('browse',[
'tvshow' => $tvshow,
'uid' => $uid,
]);
In my view:
#foreach($tvshow as $show)
{{ $show->title }}
#foreach($show->watchedtvshow as $watchedtvshow)
#if($watchedtvshow->uid == $uid)
{{'don't show checkbox'}}
#else
{{'show checkbox'}}
#endif
#endforeach
#endforeach
The problem:
The thing is the second foreach loop executes only for the times it finds a watched tv show, otherwise it doesn't. So it just won't show any checkboxes.
I'm not an experienced coder, haven't really encountered anything like this before, I've spent three whole days trying to solve this, using count, for loops and what not, but I can't. Does anybody know how to achieve this?
First off a quick pointer: you can get the logged-in user's ID with Auth::id() rather than having to set it to null, then check if they're logged in then get the id directly off the model.
As for your problem, you're quite right that using the code you have you won't be getting the full story. What you need to do is get a list of all TV shows (regardless of user having it) and additionally a list of all TV shows the user has seen. Now, you can do this many ways, but the best 'Laravel way' is to model the relationship between User and TVShow. Your code doesn't mention this so I won't assume you have already done it. Your database is, of course, already set up for this so all you need to do is create the relationship. In this case, the relationship you need is a belongsToMany (a user can 'have' (have watched) many shows, and a show can 'have' (have been watched by) many users:
// in User.php
public function shows()
{
return $this->belongsToMany('TVShow', 'watchedtvshow', 'uid', 'tvid');
}
// in TVShow.php
public function users()
{
return $this->belongsToMany('User', 'watchedtvshow', 'tvid', 'uid');
}
Once you have this you can get a list of all users that have watched a show with:
$show->users;
Or you can get a list of all shows a user has watched with:
$user->shows;
Now putting that all together you should use Laravel's collections to detect whether a given item if in both arrays:
// in the controller:
$shows = TVShow::all();
if (Auth::check()) {
$watched = Auth::user()->shows;
} else {
// just create an empty collection so we can assume a consistent API
$watched = new \Illuminate\Support\Collection;
}
return View::make('browse', compact('shows', 'watched'));
// in browse.blade.php
#foreach($shows as $show)
{{ $show->title }}
#if ($watched->contains($show)
<span class="glyphicon glyphicon-ok"></span>
#else
<span class="glyphicon glyphicon-remove"></span>
#endif
#endforeach
Something like that?

Categories