I'm working on a collection that needs to calculate some data for each row and it takes too much time to load into view. The problem is I defined an accessor and inside that accessor will perform some calculation and if the data is too big or when user retrieve too many row at once.
Example Model:
public function getCalculationAttribute()
{
$score_ids = Score::whereIn('id', $this->scores->pluck('score_id'))->pluck('id');
$count_score = $count_score->count();
$penalties = Penalty::whereIn('score_id', $score_ids->toArray())->count();
$balance = $count_score - $penalties;
$another_score = $count_score > 0 ? ($balance / $count_score) * 0.7 : 0;
return [
'field_a' => $count_score,
'field_b' => $penalties,
'field_c' => $balance,
'field_d' => $another_score
];
}
Example Controller
public function index(){
$data = ExampleModel::get();
return view('example', ['data' => $data]);
}
Example blade
#foreach($data as $row)
<p>{{ $row->calculation['field_a']}}</p>
<p>{{ $row->calculation['field_b']}}</p>
<p>{{ $row->calculation['field_c']}}</p>
<p>{{ $row->calculation['field_d']}}</p>
#endforeach
When I didn't need the calculation attribute it works perfectly fine, but when I do and I know each of them will be running query and calculation and it will take forever. Is there any good practice on retrieving data with calculation or any suggestion I can modify this to improve the performance? The code above is just an example. Thank you in advance!
You've got an N+1 query issue with this code. Each time you loop $data and call $row->calculation, you're executing 3 extra queries:
Score::whereIn(...);
$this->scores->pluck('score_id');
...
Penalty::whereIn(...);
You're calling $row->calculation 4 times... I'm pretty sure that means 12 additional queries per row in $data, since get{Whatever}Attribute() doesn't have any kind of caching/logic to know you've called it already.
If you save $row->calculations to a variable, you can reduce that a bit:
#foreach($data as $row)
#php $calculations = $row->calculations; #endphp
<p>{{ $calculations['field_a']}}</p>
<p>{{ $calculations['field_b']}}</p>
<p>{{ $calculations['field_c']}}</p>
<p>{{ $calculations['field_d']}}</p>
#endforeach
Additionally, you can eager load the scores relationship to reduce it a bit more:
$data = ExampleModel::with('scores')->get();
Including that will make $this->scores->pluck('score_id'); use the pre-loaded data, and not call an additional query.
Lastly, try to use relationships for your Score::whereIn() and Penalty::whereIn() queries. I'm not sure how you would define them, but if you did, then including those in your ->with() clause will hopefully completely remove this N+1 query issue.
Related
I'm trying to understand Eager Loading using Laravel to avoid generating a lot of unnecessary queries. I want to get 15 last added Posts and also get their rates from relationship of my rates table (before I was getting Posts and later in foreach I was calling for $item->avgRate() that creates 15 additional queries :S).
My Post model:
public function rates()
{
return $this->hasMany(Rate::class);
}
public function scopeLastAdded($query, $limit = 15)
{
return $query->latest()->limit($limit)->with('rates')->get();
}
This works, for each post, I'm also getting all rates, but the main goal is to make some function to calculate avg rate for each post and not retrieve all rates. I created a new method:
public function avgRate()
{
return number_format($this->rates()->avg('rate'), 1, '.', '');
}
When I use with('avgRate') my model fails:
Call to a member function addEagerConstraints() on string
How can I get avgRate in some clean way with my last 15 Posts to perform only 2 queries and not 16?
Expected output:
// Post view
#foreach ($posts as $post)
<div>{{ $post->title }}</div>
<div>{{ $post->avgRate }}</div> //I want to get data without performing 15 queries
#endforeach
I would use a subquery to achieve this. Also, to make things a little bit cleaner, you can create a scope for fetching the rating:
public function scopeWithRating($query)
{
$rating = Rate::selectRaw('AVG(rate)')
->whereColumn('post_id', 'posts.id')
->getQuery();
$query->select('posts.*')
->selectSub($rating, 'rating');
}
... and to use it, you'd do:
Post::withRating()->get();
Now, your Post objects will also contain a column rating, and that has been done with, essentially, a single query.
Here's an example to illustrate this.
I need your help to build a query in Laravel either eloquent or DB query would do too.
My table name is Users
To see the DB structure, open the following link:
https://jsfiddle.net/vardaam/mvqzpb2j/
Every user row has 2 columns referral_code and referred_by_code which means every user can refer to someone and earn bonus similarly the same user was also been referred by some one.
I would like to return the information on page with loop in users details along with Username of the user who had referred him to this. To track the same I created those 2 columns in the same table i.e.: referral_code and referred_by_code.
I do not know how to write this in 1 query or how to combine 2 queries and get the desired results.
My controller code looks like below:
$obj_users = User::get();
$codes = [];
$referrers = [];
foreach( $obj_users as $referred_by_code )
{
//Following fetches all user's referred_by_code's code
$codes[] = $referred_by_code->referred_by_code;
}
foreach ($codes as $code)
{
//Following fetches usernames of the given referred_by_code's codes
$referrers[] = User::where('referral_code', $code)->first()->username;
}
return view('users.users', compact(['users', 'paginate', 'referrers']));
The returning $users variable provides me loop of users data but I do not know how to attach those referrer's username to that object.
I tried my level best to describe, please ask incase what I said doesn't make sense, will be happy to provide further clarification.
Best
You can add into your User model the following relationship:
public function referredBy()
{
return $this->belongsTo(User::class, 'referred_code', 'referral_code');
}
In your controller you can use:
$users = User::with('referredBy')->get();
return view('users.users', compact('users'));
and later in your view you can use:
#foreach ($users as $user)
{{ $user->username }} referred by {{ $user->referredBy ? $user->referredBy->username : '-' }}
#endforeach
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 }}
What is the Correct Way to retrieve a column value based on certain select filter on a Model variable availed by compact method inside the blade. (Larevl 5)
I read that Its a bad practice to query database staright from views, and hence i followed the convention to avail the required data with compact method to view
However, In a scenario where I need to query another table based on certain column value returned in foreach loop inside a blade from first table, I am unable to figure out correct Approach
Example: I have two Models User & Group
Schema User Table
id,name,email,group_id
Scheme Group Table
id,groupname
Here is the UserController -> compact method
$users = \App\User::all(array('id','name','email','group_id'));
$groups = \App\Group::all(array('id','group_name'));
return view('user.index', compact('users','groups'));
Here how the blade needs them
#foreach ($users as $user)
<tr>
<th>{{$user->id}}</th>
<th>{{$user->name}}</th>
<th>{{$user->email}}</th>
<th>
<!-- Here i need to run something like
select group_name from group where id = $user->id -->
{{$groups->where('id','=',$user->group_id) }}
</th>
<th>Actions</th>
</tr>
#endforeach
I know this returns an array , and I'have two questions here
How to get the value for group_name column from the Group Model based on group.id = $user->id in a foreach loop
Since Its a bad practice to query db from blade, how would I avail the values from a model by passing data via compact from controller to blade, when the where clause parameter's are not yet known.
Edit 1:
I modified the last group query as
<th>#if($groups->where('id','=',$user->group_id))
#foreach($groups as $group)
{{$group->group_name}}
#endforeach
#endif
</th>
And I was able to get the result, however this again isn't a correct approach , so question remain unanswered
In User model
public function group()
{
return $this->belongsTo('App\Group');
}
In Group model
public function users()
{
return $this->hasMany('App\User');
}
In your controller
$users = \App\User::with('group')->get();
return view('user.index', compact('users'));
Now in your view you can do
$user->group->name;
I appreciate the fact that you know "It's bad practice to query from view".
Why don't you use join.
DB::table('users')->join('groups', 'users.group_id', '=', 'groups.id')->get();
Then pass the result to your view and loop through it.
Here you will have each user data associated with his group data.
I'm trying to return multiple views using the laravel framework. When I return the variable, it only makes it through the loop once, therefore only one comment is displayed on the page.
foreach($index_comments as $comments){
$commentComment = $comments->comment;
$index_children = NULL;
$getUser = DB::table('users')->where('id', '=', $comments->from_user_id)->get();
foreach ($getUser as $user) {
$firstName = $user->first_name;
$lastName = $user->last_name;
}
return View::make('feeds.comments')->with(array(
'firstName' => $firstName,
'lastName' => $lastName,
'commentComment' => $commentComment,
'index_children' => $index_children
));
}
I just need a way of returning multiple views.
Thanks for any help!
Toby.
It seems that you don't quite understand the concepts of Laravel and/or PHP yet. So let's start it from scratch: We want to fetch all comments, output the comment and the name of the user who wrote the comment.
At a very basic level, we can just grab it straight from the DB with the query builder:
public function showComments()
{
$commentData = DB::table('comments')
->join('users', 'users.id', '=', 'comments.from_user_id')
->get(['text', 'firstName', 'lastName']);
return View::make('feeds.comments')->with('commentData', $commentData)
}
And in your view:
#foreach($commentData as $comment)
{{ $comment->text }}
<br />
written by {{ $comment->firstName }} {{ $comment->lastName }}
<hr />
#endforeach
That's it. You don't return the view on each iteration, the iteration happens in the view. The return statement terminates the function execution immediately. If you return within in a loop, it will always exit upon the first iteration, that's why you're getting only one result.
In the next step, you should play around with Models and Eloquent for even more powerful and readable data handling.