Laravel 5.3 - multiple db queries when loading relations - laravel

I have posts table (id, user_id, title) and Post model with this content
class Post extends Model
{
public function user()
{
return $this->belongsTo('App\User');
}
}
I want to get some post by id and also the user's information, so I use this query
$post = new Post();
$res = $post->where('id', 1)->select('id', 'title', 'user_id')->with([
'user' => function ($query) {
$query->select('id', 'name', 'email');
}
])->first();
It returns the data as expected, and i can access the post's info like $res->title, or the user's info like $res->user->email, but the problem is it makes 2 queries to the database
I would expect to have one query only
SELECT
`posts`.`id`,
`posts`.`title`,
`posts`.`user_id`,
`users`.`id`,
`users`.`email`,
`users`.`name`
FROM
`posts`
LEFT JOIN `users`
ON `posts`.`user_id` = `users`.`id`
WHERE `posts`.`id` = '1'
LIMIT 1
Please note, this is not the same as N+1 problem
https://laravel.com/docs/5.3/eloquent-relationships#eager-loading
I know I can manually do left join,
$res = $post->where('posts.id', 1)
->select('posts.id', 'posts.title', 'posts.user_id', 'users.email', 'users.name')
->leftJoin('users', 'posts.user_id', '=', 'users.id')
->first();
and it will have the one query as I need, but the problem is in the result all data from related table is in the same array (and besides, what is the point of defining/using relationships if i have to manually make a left join every time)
So, my question is how to get the post data with related tables with one query and result organized according to relations: I am curious what is the best practice in laravel and how experienced Laravel developers are doing this ?
Thanks

Eloquent never uses JOINs to retrieve relationship data, but instead uses seperate queries and links the data together in PHP objects. Therefore, you will always have one extra query for each relationship. Also, Eloquent mostly loads all columns (using *).
To link them together, you have to stop using the query builder and instead use Eloquent directly:
$post = Post::find(1)->load('user');
If you insist on using JOINs, you will have to continue using the query builder.

That is eager loading.
You are using
->with([
'user' => function ($query) {
$query->select('id', 'name', 'email');
}
])
In eager loading, what happens is first run above query and get all the users matching the query.
Then the result is applied to the outer query which is
$post->where('id', 1)->select('id', 'title', 'user_id')->with([
'user' => function ($query) {
$query->select('id', 'name', 'email');
}
])->first();

Related

How to translate the SQL query to Laravel Eloquent query?

I'm trying to make a complex query using Laravel Eloquent. I know how to do it using raw SQL query, but I don't have any idea how to do it using Eloquent.
Here is my SQL query, and it works perfectly:
select *
from students
where exists(select *
from (select student_movements.id AS sm_id, student_movements.direction, student_movements.deleted_at
from student_movements
inner join student_student_movements
on student_movements.id = student_student_movements.student_movement_id
where students.id = student_student_movements.student_id
and student_movements.deleted_at is null
order by student_movements.id desc
limit 1) as last_sm
where last_sm.direction = 1 AND last_sm.date >= 5-5-2022
);
My models have many-to-many relation using student_student_movements table:
Student
public function studentMovements(): BelongsToMany
{
return $this->belongsToMany(
StudentMovement::class,
'student_student_movements',
);
}
StudentMovement
public function students(): BelongsToMany
{
return $this->belongsToMany(
Student::class,
'student_student_movements'
);
}
My goal is to get all Students, who have the last Movement where direction = 1 and the date of the last Movement >= $someDate.
So, my question is: how to translate the SQL query to Eloquent? I saw many similar questions, but they are not helping me.
Thanks for any advice.
Use the whereHas method, then fine tune the sub query inside the closure to your needs.
You can use the whereHas and orWhereHas methods to define
additional query constraints on your has queries.
There is an example like that in laravel documentation
use Illuminate\Database\Eloquent\Builder;
// Retrieve posts with at least one comment containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
// Retrieve posts with at least ten comments containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, '>=', 10)->get();
check the documentation here

Laravel eloquent filter on hasMany relationship

I am trying to filter on the field which has many relationships.
This is how my query looks:
Order::with(['fullOrderItems' => function($query) use ($branch) {
$query
->where('branch', $branch->key);
}])
->orderBy('dateCreated')
->toSql();
The query looks like this once outputted:
select * from `Order` where `stage` <> ? order by `dateCreated` asc
And it seems like the query doesn't even consider the nested query for filtering the branch.
My relationship in Order class is defined like this:
/*
*
* #return HasMany|null
*/
public function fullOrderItemss() : ?HasMany
{
return $this->hasMany(OrderItems::class, 'order', 'key');
}
Do you see what am I doing wrong here? How can I get the full query (including nested condition)?
To filter the Entity order with a relation you need to use whereHas
Order::whereHas('fullOrderItems', function($query) use ($branch) {
$query
->where('branch', $branch->key);
})
->with(['fullOrderItems' => function($query) use ($branch) {
$query
->where('branch', $branch->key);
}])
->orderBy('dateCreated')
->get();
You can for example get the order filtered by branch id but get all the fullOrderItems (ingore the branch id) of those fullOrderItems like this
Order::whereHas('fullOrderItems', function($query) use ($branch) {
$query
->where('branch', $branch->key);
})
->with('fullOrderItems')
->orderBy('dateCreated')
->get();
This last example will make it simpler to understand the difference between the two filters.
For why the with condition doesnt show on the query:
it is used on a second unique query that fetchs the relation using the ids of orders retrieved in the first query. that way you get the orders, each with their respective fullOrderItems with only two queries.

Laravel Eloquent with() selecting specific column doesn't return results

Say I have 2 models, Category and POI where 1 Category can have many POIs.
$categoryDetails = Category::with([
'pois' => function ($query) {
$query->where('is_poi_enabled', true);
},
])->findOrFail($id);
The above query returns results from the specific Category as well as its POIs.
However, with the query below:
$query->select('id', 'name')->where('is_poi_enabled', true);
The POIs become empty in the collection.
Any idea why this is happening? When added a select clause to the Eloquent ORM?
While doing a select it's required to fetch the Relationship local or Primary key.
For an example POIs table contains category_id then it's required to select it
Try this:
$categoryDetails = Category::with([
'pois' => function ($query) {
$query->select(['id', 'category_id', 'is_poi_enabled'])
->where('is_poi_enabled', true);
},
])->findOrFail($id);
Good luck!

Laravel with-count subquery without relation

My target is to get collection of books with count of matches book.names in another table without relation
I get collection like this
$books = Books::paginate(20);
What I need now is to get the count of matches like this
SELECT COUNT(*) FROM `posts` WHERE `body` LIKE '%book.name%'
How can I do this with one query and avoiding unnecessary queries for each model, like eager loading
You can do it with eager loading without loading all post. There is a method called withCount
Books.php
public function posts() {
return $this->hasMany(Post::class, 'post_id', 'id');
}
One way to find all post related to book is
$books = Books::withCount(['posts' => function ($query) use($searchTerm) {
$query->where('body', 'like', $searchTerm);
}])->get();
//How to retrieve
$book = $books->first()->posts_count.
You can find more information about withCount on laravel documentation website.
Approach 2: Without Eager Loading
$books = Books::select('*')->addSelect(DB::raw("SELECT COUNT(*) as post_count FROM `posts` WHERE `body` LIKE '%book.name%' ")->get(); //This will return collection with post_count.
Note: Not tested

Laravel Eloquent ORM eager loading. Relation incorrectly returned as null

I have an Eloquent ORM relationship defined as follows:
ProductConfiguration:
public function product()
{
return $this->belongsTo('Excel\Products\Product');
}
public function currency()
{
return $this->belongsTo('Excel\Currencies\Currency');
}
Product
public function productConfigurations()
{
return $this->hasMany('Excel\Products\ProductConfiguration');
}
public function productType()
{
return $this->belongsTo('Excel\Products\ProductType');
}
I expect that if I do the following that I will load all product configurations of a specified product type, with the related products, nested product type details and the product configuration currency
$results = ProductConfiguration::with(
array(
'product' => function($query) use ($product_type) {
$query->where('product_type_id' , $product_type);
},
'product.productType',
'currency'
)
)
->get();
however the returned collection has 'product' set to NULL. the Currency Relationship is there, but the product relationship is not. I can see the outputted SQL queries and the query that selects the products retrieves the correct products if I paste it directly into my sql editor
select * from `products`
where `products`.`id` in ('12', '13')
and `product_type_id` = '1'
Am I correct to think that the results from this query should be included in my collection, or is there some obvious flaw in my thinking?
I think you don't want to achieve that. Now what you get is getting all ProductConfiguration with products that are only of certain_type.
So in case you have some configuration that has other type for product you will get null because you limited results from product to only the one that has certain product type.
I might be wrong, but you probably wanted to get those ProductConfiguration that belongs to Product that is type of certain_type. In this case you should use whereHas:
$results = ProductConfiguration::
with('product', 'product.productType', 'currency')->
whereHas('product', function($q) use ($product_type)
{
$q->where('product_type_id', '=', $product_type);
})->get();
I hate to post this as an answer but since i don't have enough rep to comment so try this first:
$results = ProductConfiguration::with('product')->get();
dd($results->toArray());
See what you get, if you get some data, try this
$results = ProductConfiguartion::with(array('products' => function($query){
$query->where('product_type_id' , $product_type);
})->get();
dd($results);
See what you get, if you get null: your $product_type variable may be something you didnt expect, so try dd($product_type) to make sure its what your expecting.

Resources