Laravel - Eager Loading - php

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.

Related

Retrieve collection with calculation takes too long to load (Laravel)

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.

Manipulating data within an object

Hi I am new to Laravel and php. In my database each student has three marks on each subject and I need to retrieve them and send to the view the average mark for each student.
I tried doing it like below, but the returned value is an object (e.g. [5,4,3]) and it doesn't let me count the average. Please advise how I can operate with data within the object.
$students = Student::all();
foreach ($students as $student) {
$mathPoints = Point:: where('subject_id', 1)
->where('student_id', $student->id)
->pluck('points');
}
I tried turning it into an array by (array) method, but I couldn't calculate the sum of values with array_sum after.
Update: my Point model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Point extends Model
{
//Get the student the credit points are related to
public function student()
{
return $this->belongsTo('App/Models/Student');
}
//Get the subject the credit points are related to
public function subject()
{
return $this->belongsTo('App/Models/Subject');
}
}
use Model::avg('columnName') to calculate average .
read more here : https://laravel.com/docs/5.4/queries#aggregates
$students = Student::all();
foreach ($students as $student){
$mathPoints = Point::where(['subject_id'=>1,'student_id'=>$student->id])->avg('points');
$student->avgPoint=$mathPoints;
}
inside your blade :
{{ $student->avgPoint}}
Point::where('student_id', $student->id)->get()
When you use all() with Eloquent, it makes the query for you. If you use where(), you have to ‘get’ it using get() before you can use it as a collection.
$mathPoints = Point::where('student_id', $student->id)
->get()
->pluck('points');
However, I’d probably look at using more complicated queries to fetch this data, as you could easily end up making hundreds of queries per page, rather than just 1.

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

Eloquent HasMany relationship, with limited record count

I want to limit related records from
$categories = Category::with('exams')->get();
this will get me exams from all categories but what i would like is to get 5 exams from one category and for each category.
Category Model
public function Exams() {
return $this->hasMany('Exam');
}
Exam Model
public function category () {
return $this->belongsTo('Category');
}
I have tried couple of things but couldnt get it to work
First what i found is something like this
$categories = Category::with(['exams' => function($exams){
$exams->limit(5);
}])->get();
But the problem with this is it will only get me 5 records from all categories. Also i have tried to add limit to Category model
public function Exams() {
return $this->hasMany('Exam')->limit(5);
}
But this doesnt do anything and returns as tough it didnt have limit 5.
So is there a way i could do this with Eloquent or should i simply load everything (would like to pass on that) and use break with foreach?
There is no way to do this using Eloquent's eager loading. The options you have are:
Fetch categories with all examps and take only 5 exams for each of them:
$categories = Category::with('exams')->get()->map(function($category) {
$category->exams = $category->exams->take(5);
return $category;
});
It should be ok, as long as you do not have too much exam data in your database - "too much" will vary between projects, just best try and see if it's fast enough for you.
Fetch only categories and then fetch 5 exams for each of them with $category->exams. This will result in more queries being executed - one additional query per fetched category.
I just insert small logic inside it which is working for me.
$categories = Category::with('exams');
Step 1: I count the records which are coming in response
$totalRecordCount = $categories->count()
Step 2: Pass total count inside the with function
$categories->with([
'exams' => function($query) use($totalRecordCount){
$query->take(5*$totalRecordCount);
}
])
Step 3: Now you can retrieve the result as per requirement
$categories->get();

How to retrieve information from an association table using Idiorm and Paris with only one query?

I am using Paris (which is built on top of Idiorm).
I have the following Model classes (example inspired from the documentation on github):
<?php
class Post extends Model {
public function user() {
return $this->belongs_to('User');
}
}
class User extends Model {
public function posts() {
return $this->has_many('Post'); // Note we use the model name literally - not a pluralised version
}
}
So now I can do the following (works well):
// Select a particular user from the database
$user = Model::factory('User')->find_one($user_id);
// Find the posts associated with the user
$posts = $user->posts()->find_many();
// Select a particular post from the database
$post = Model::factory('Post')->find_one($post_id);
// Find the user associated with the post
$user = $post->user()->find_one();
But I'd like to do the following aswell:
$posts = Model::factory('Post')->find_many();
foreach ($posts as $post) {
echo($post->user()->find_one()->username); // generates a query each iteration
}
Unfortunately this creates a query for each iteration. Is there a way to tell Paris or Idiorm to take the associated information in the first find_many query ?
How are you supposed to retrieve information with Paris to minimize the numbers of query ? I'd like not to have to manually specify the join condition (this is the reason why I am using Paris and not Idiorm)
I am the current maintainer of the Paris and Idiorm projects. Unfortunately what you describe here is a case for a custom query and not something that Paris was ever built to solve. The philosophy of Paris and Idiorm is to stick as close to the 80/20 principle as possible and your use case is in the 20.
It would be interesting to know how you ended up solving this problem.
I've had exactly the same problem, and ended up with this (a bit ugly) solution:
$posts = Model::factory('Post')...->limit($perpage)->find_many();
$user_ids = $user_lookup = array();
foreach($posts as $post) $user_ids[] = $post->user_id;
$users = Model::factory('User')->where_id_in($user_ids)->find_many();
foreach($users as $user) $user_lookup[$user->id] = $user;
Only 2 selects. And later in template:
{% for post in posts %}
<h2>{{ post.title }}</h2>
by author: {{ user_lookup[post.user_id].username }}
{% endfor %}
But it only works if you don't have hundreds of posts showing on one page.

Categories