I'm trying to use the orWhereHas method in Laravel with nested relations, but I am not getting the result I expect. Am I misunderstanding how to use this method?
My relationships are set up as follows:
Product
-> hasOne uniqueItem
-> hasMany fulfillmentCenterUniqueItems
-> hasMany skus
-> hasOne uniqueItem
-> hasMany fulfillmentCenterUniqueItems
I'm trying to test out the whereHas and orWhereHas methods by retrieving products from the database that contain uniqueItems that have a uniqueItemFulfillmentCenter with id = 7089 OR products that contain a sku, that contains a uniqueItem, that has a uniqueItemFulfillmentCenter with id = 7412.
Based on the data in my database, the result of this query should be two products. Product IDs 105 and 239.
Here's the Eloquent code I'm using:
$product = Spire_models\Product
::whereHas('uniqueItem.fulfillmentCenterUniqueItems', function ($query)
{
$query->where('id', 7089);
})
->orWhereHas('skus.uniqueItem.fulfillmentCenterUniqueItems', function ($query)
{
$query->where('id', 7412);
})
->get()->toArray();
For some reason, this is only returning product ID 105, instead of 105 and 239. The generated sql from this function is:
select * from `products` where `products`.`deleted_at` is null and (select count(*) from `unique_items` where `products`.`unique_item_id` = `unique_items`.`id` and (select count(*) from `fulfillment_center_unique_items` where `fulfillment_center_unique_items`.`unique_item_id` = `unique_items`.`id` and `id` = 7089 and `fulfillment_center_unique_items`.`deleted_at` is null) >= 1 and `unique_items`.`deleted_at` is null) >= 1 and (select count(*) from `skus` where `skus`.`product_id` = `products`.`id` and (select count(*) from `unique_items` where `skus`.`unique_item_id` = `unique_items`.`id` or (select count(*) from `fulfillment_center_unique_items` where `fulfillment_center_unique_items`.`unique_item_id` = `unique_items`.`id` and `id` = 7412 and `fulfillment_center_unique_items`.`deleted_at` is null) >= 1 and `unique_items`.`deleted_at` is null) >= 1 and `skus`.`deleted_at` is null) >= 1
Is this sql being generated incorrectly, or am I misusing the orWhereHas method? To me it does not look like the OR statement is being placed correctly in the sql.
If I remove the orWhereHas method, things works as expected. For example, if I run this:
$product = Spire_models\Product
::whereHas('uniqueItem.fulfillmentCenterUniqueItems', function ($query)
{
$query->where('id', 7089);
})
->get()->toArray();
I correctly get back product ID 105. If I run this:
$product = Spire_models\Product
::whereHas('skus.uniqueItem.fulfillmentCenterUniqueItems', function ($query)
{
$query->where('id', 7412);
})
->get()->toArray();
I correctly get back product ID 239. So the individual pieces of the query work correctly, but it seems when I try to combine these with an orWhereHas, I get unexpected results. Any idea why?
EDIT
As per the comments, it looks like this is a bug. I was able to temporarily work around it by rewriting the code to use where and orWhere. Here's the temporary solution:
$product = Spire_models\Product
::where(function ($query)
{
$query->whereHas('uniqueItem.fulfillmentCenterUniqueItems', function ($query)
{
$query->where('id', 7089);
});
})
->orWhere(function ($query)
{
$query->whereHas('skus.uniqueItem.fulfillmentCenterUniqueItems', function ($query)
{
$query->where('id', 7412);
});
})
->get()->toArray();
It was a bug and is fixed by now with this PR https://github.com/laravel/framework/pull/8171
It's been OK since version 5.0.21
Related
Here is the query I tried to write and gives an error
$users =User::has('subscriptions', function (Builder $q) {
$q->whereNotNull('ends_at');
})->get();
Getting this error
SQLSTATE[42601]: Syntax error: 7 ERROR: SELECT * with no tables specified is not valid LINE 1: ...sers"."id" = "subscriptions"."user_id") = (select * where "e... ^ (SQL: select * from "users" where (select count(*) from "subscriptions" where "users"."id" = "subscriptions"."user_id") = (select * where "ends_at" > now) and "users"."deleted_at" is null)
When I write this code I get results but need to filter result to get a list of subscribed users without calling User::all() then loop to filter.
User::has('subscriptions')->get();
use
$users = User::with(['subscriptions' => static function ($query) {
$query->whereNotNull('ends_at');
}])->get();
Read Constraining Eager Loads
To query, you need to load the subscriptions relationship first.
Solution is here 😁
$users = User::whereHas('subscriptions', function (Builder $q) {
return $q->active();
})->get()->toArray();
Have a good day
Do it like this
$users = User::with(['subscriptions' => static function ($query) { $query->whereNotNull('ends_at'); }])->get();
$users = User::with(['subscriptions' => function ($query) {
return $query->whereNotNull('ends_at');
}])->get();
Or,
$users = User::with('subscriptions')->whereHas('subscriptions', function ($query) {
return $query->whereNotNull('ends_at');
})->get();
This will give you only subscribed users. But if you fetch all the users and then apply filter to the fetched result, then every row will be fetched from the database, which is not a good practice.
SELECT
posts.id,
(select count(*) from post_likes where post_id = 13 and user_id = 12) as post_like
FROM
posts
LIMIT 5
How to write this query in Laravel query builder?
If your ORM models are defined (and you have both Post and PostLike models), create a relationship in your Post.php model (if not already), like:
public function likes(){
return $this->hasMany(PostLike::class);
}
Then if you only need the count, try something like:
$userId = 12;
$postList = Post::query()
->whereId(13)
->withCount(['likes', 'likes AS post_like' => function ($query) use($userId) {
$query->where('user_id', '=', $userId);
}])
->limit(5)
->get();
// Then do something with result.
foreach ($postList as $post) {
$count = $post['post_like'];
}
Note that above we use post_like alias, and limit to user_id, just to much OP requirements; Else we could simply set likes_count to the number of relations, like:
->withCount('likes')
But you could use relationship for subquery with the whereHas(...) eloquent method, like:
Post::query()->whereHas('likes', function($query){
$query->where(...your statements for sub query go there);
}, '>', 4)->limit(5)->get(); //Select where more than 4 relation found with given parameters
For more see: https://laravel.com/docs/8.x/eloquent-relationships#querying-relationship-existence
So I have Status class which has pivot table relationship with roles:
public function roles():
{
return $this->belongsToMany(Role::class, 'status_role', 'status_id', 'role_id');
}
This is how Status db table looks:
id title
1 status1
2 status2
3 status3
And then my pivot table which looks like this:
status_id role_id
1 2
2 2
And now I want to write query which returns statuses with role_id=2.
Basically it should return data like this: status1, status2 and not include status3.
What I have tryed:
$statuses = Status::query()
->leftJoin('status_role', function ($join) {
$join->on('statuses.id', '=', 'status_role.status_id')
->whereIn('status_role.role_id',[2]);
})
->get();
But now it returns all statuses (status1, status2, status3) it should be only (status1 and status2). How I need to change it?
This query will return all statuses attached to roles with id 2:
Status::query()->whereHas('roles', function($q){
$q->where('id', 2);
})->get();
It uses the whereHas method that can be useful when you need to query relationships.
It can do a lot more, you should check the documentation on this topic: https://laravel.com/docs/8.x/eloquent-relationships#querying-relationship-existence
Quick note: whereHas is the "Laravel preferred way" of doing what you are trying to achieve.
However, you should be able to also do it with this query, which is closer to your current code:
$statuses = Status::query()
->join('status_role', function ($join) {
$join
->on('statuses.id', '=', 'status_role.status_id')
->where('status_role.role_id',2);
})
->get();
// I replaced the leftJoin by join, which will exclude all results without roles (e.g. status id 3)
// or even simpler:
$statuses = Status::query()
->join('status_role', 'statuses.id', '=', 'status_role.status_id')
->where('status_role.role_id',2)
->get();
I am using laravel 8. I have this mysql command which I want to convert into laravel query builder style:
select allocation.*, leav_leave_types.leave_type_code
from (
select * from leav_employee_annual_leave_allocations
where leave_year_id = $year_id and employee_id = $user_id
) as allocation
left join leav_leave_types on (leav_leave_types.id = allocation.leave_type_id)
Actually I want to apply a where clause first and then perform a left join for better performance.
How can I convert it into query builder style?
The only thing from your query that is not currently in the documentation is using a subquery as the main table.
This can be done by passing either a Closure or a Builder instance to the table() or from() method.
DB::table(closure, alias)
DB::table(builder, alias)
DB::query()->from(closure, alias)
DB::query()->from(builder, alias)
Using a Closure:
DB::table(function ($sub) use ($user_id, $year_id) {
$sub->from('leav_employee_annual_leave_allocations')
->where('leave_year', $year_id)
->where('employee_id', $user_id);
}, 'allocation')
->select('allocation.*', 'leav_leave_types.leave_type_code')
->leftJoin('leav_leave_types', 'leav_leave_types.id', 'allocation.leave_type_id')
->get();
DB::query()
->select('allocation.*', 'leav_leave_types.leave_type_code')
->from(function ($sub) use ($user_id, $year_id) {
$sub->from('leav_employee_annual_leave_allocations')
->where('leave_year', $year_id)
->where('employee_id', $user_id);
}, 'allocation')
->leftJoin('leav_leave_types', 'leav_leave_types.id', 'allocation.leave_type_id')
->get();
Using a Builder instance
$sub = DB::table('leav_employee_annual_leave_allocations') // or DB::query()->from('leav_employee_annual_leave_allocations')
->where('leave_year', $year_id)
->where('employee_id', $user_id);
DB::table($sub, 'allocation')
->select('allocation.*', 'leav_leave_types.leave_type_code')
->leftJoin('leav_leave_types', 'leav_leave_types.id', 'allocation.leave_type_id')
->get();
// personally my favorite way. I find it very readable.
$sub = DB::table('leav_employee_annual_leave_allocations') // or DB::query()->from('leav_employee_annual_leave_allocations')
->where('leave_year', $year_id)
->where('employee_id', $user_id);
DB::query()
->select('allocation.*', 'leav_leave_types.leave_type_code')
->from($sub, 'allocation')
->leftJoin('leav_leave_types', 'leav_leave_types.id', 'allocation.leave_type_id')
->get();
The generated SQL looks like this
select "allocation".*, "leav_leave_types"."leave_type_code" from (
select * from "leav_employee_annual_leave_allocations"
where "leave_year" = ? and "employee_id" = ?
) as "allocation"
left join "leav_leave_types" on "leav_leave_types"."id" = "allocation"."leave_type_id"
If you want a parenthesis around your join condition to be generated, you should use one of the following notations instead.
leftJoin('leav_leave_types', ['leav_leave_types.id' => 'allocation.leave_type_id'])
leftJoin('leav_leave_types', function ($join) {
$join->on(['leav_leave_types.id' => 'allocation.leave_type_id']);
})
leftJoin('leav_leave_types', function ($join) {
// will generate a parenthesis if there's more than one condition
$join->on('leav_leave_types.id', 'allocation.leave_type_id')
->on(...) // and condition
->orOn(...); // or condition
})
Alternatively, you could turn the SQL around to
select *,
( SELECT leave_type_code
FROM leav_leave_types
WHERE id = allocation.leave_type_id
) AS leave_type_code
FROM leav_employee_annual_leave_allocations AS allocation
where leave_year_id = $year_id and employee_id = $user_id
(This might be more efficient.)
In either case leav_employee_annual_leave_allocations would benefit from INDEX(employee_id, leave_year_id).
I have 2 tables
risks
risk_id name area_id
1 a 1
2 b 1
risk_areas
id name parent_id
1 a 0
2 b 1
3 c 1
4 d 2
This is my sql query:
SELECT * FROM `risks` WHERE (`register_id` = 29 AND `area_id` = 1)
OR (`area_id` IN (SELECT id FROM risk_areas WHERE parent_id = 1) AND `register_id` = 29 )
OR (`area_id` IN (SELECT id FROM risk_areas WHERE parent_id IN (SELECT id FROM risk_areas WHERE parent_id = 1)) AND `register_id` = 29 )
When run in phpMyAdmin the query works as expected.
I've been trying to implement it into Laravel using the query builder. It fetches a risk from a certain register (I've used 29 for an example) and also finds all of its children risks (there can only be up to 3 generations e.g parent->child->child)
I intially tried this:
$query-> raw("
SELECT * FROM `risks` WHERE (`register_id` = " . $q['register_id'] . " AND `area_id` = ".$q['area_id'].")
OR (`area_id` IN (SELECT id FROM risk_areas WHERE parent_id = ".$q['area_id'].") AND `register_id` = " . $q['register_id'] . " )
OR (`area_id` IN (SELECT id FROM risk_areas WHERE parent_id IN (SELECT id FROM risk_areas WHERE parent_id = ".$q['area_id'].")) AND `register_id` = " . $q['register_id'] . " )
");
$q['register_id'] fetches the register_id and $q['area_id'] fetches the area_id correctly.
Since that didn't work (it just outputted the parents and even the ones with the same area_id but different register ids) I tried this:
$query
->where('area_id', $q['area_id'])
->orWhere('area_id','IN', function ($query) use($q) {
$query->where('parent_id',$q['area_id'])
->where('register_id',$q['register_id']);
})
->orWhere('area_id','IN', function ($query) use($q) {
$query->where('parent_id','IN', function ($query) use($q) {
$query->where('parent_id',$q['area_id']);
})
->where('register_id',$q['register_id']);
})
->where('register_id',$q['register_id']);
But this did the same as the first. My 3rd attempt:
$query
->where('area_id', $q['area_id'])
->orWhere('area_id','IN', function ($query) use($q) {
$query->DB::table('risk_areas')
->select(DB::raw('id'))
->where('parent_id',$q['area_id'])
->where('register_id',$q['register_id']);
})
->orWhere('area_id','IN', function ($query) use($q) {
$query->DB::table('risk_areas')
->select(DB::raw('id'))
->where('parent_id','IN', function ($query) use($q) {
$query->DB::table('risk_areas')
->select(DB::raw('id'))
->where('parent_id',$q['area_id']);
})
->where('register_id',$q['register_id']);
})
->where('register_id',$q['register_id'])->get();
But this does the same as the first. How can I replicate the sql query?
EDIT:
Each register has its own register_id (this is only used to tighten the search).
Each register contains many risks.
Each risk can only have one risk_area.
I have got the same results each time, whether feeding a raw sql query or using the query builder.
The only time where I got different(and correct) results was when I fed the query into phpmyadmin (hence why I want to replicate the working query in my code).
The variables all have correct values as I have checked using dd().
I think the reason is that my syntax on the query builder is incorrect, which is why I ask how to convert my sql query into the laravel query builder.
EDIT 2:
The raw query outputs all the risks for a specific register (all the risks displayed belong to that register). When I hard code values into the raw query, I get the same result (whatever register_id/area_id combo I put in, I will get all the results for that register as this is the default view for the page.). This makes me think that maybe the raw query isn't being processed properly?
To translate WHERE IN (SELECT ... ), you need to use whereIn.
Also, trying to use DB::table() inside a subquery won't yield the result you expect.
This:
.., function ($query) use($q) {
$query->DB::table('risk_areas')
...
})
Should be:
.., function ($query) use($q) {
$query->from('risk_areas')
...
})
The translated query should then, end up like this:
DB::table('risks')
->where(function ($query) {
$query->where('register_id', 29)
->where('area_id', 1);
})
->orWhere(function ($query) {
$query->whereIn('area_id', function ($sQuery) {
$sQuery->select('id')
->from('risk_areas')
->where('parent_id', 1);
})
->where('register_id', 29);
})
->orWhere(function ($query) {
$query->whereIn('area_id', function ($sQuery) {
$sQuery->select('id')
->from('risk_areas')
->whereIn('parent_id', function ($sQuery2) {
$sQuery2->select('id')
->from('risk_areas')
->where('parent_id', 1);
});
})
->where('register_id', 29);
})
->get();