SQL query to Laravel query builder - php

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();

Related

Laravel Eloquent query siblings in same table with eager loading

I have a parent table called patients which has a one-to-many relationship with a child table called notes. (i.e. One patient can have several notes). If given a note, I would like to find other notes for the same patient. Notes are related to patients by a fk called patient_id.
In SQL, I'd do this:
SELECT * FROM notes WHERE patient_id={note.patient_id} AND id <> {note.id}
In Eloquent, I have this:
class Note extends Model
{
public function otherEncounterNotes()
{
return $this->hasMany('App\Note', 'patient_id', 'patient_id')->where('id', '<>',$this->id);
}
...
In my database, the patient with id=1 has two notes with ids 1 and 2, so if I look for the siblings of note id 1, I should get note id 2.
When I use find(), it works as expected, but when I use where(), it returns the original note instead of the sibling. Any ideas?
>>> Note::find(1)->otherEncounterNotes->pluck('id')
=> Illuminate\Support\Collection {#5542
all: [
2,
],
}
>>> Note::where('id',1)->with('otherEncounterNotes')->pluck('id')
=> Illuminate\Support\Collection {#5526
all: [
1,
],
}
Given a Note id, you could obtain the results you want by using the relationship with the Patient model.
$note_id = 1;
// "Pretty" syntax, but it's 3 queries
$sibling_notes = Note::find($note_id) // Query 1
->patient // Query 2
->notes()->where('id', '<>', $note_id)->pluck('id'); // Query 3
Or using a subquery
$note_id = 1;
// A bit messier, 1 query + 1 subquery
$sibling_notes = Note::where('id', '<>', $note_id)
->where('patient_id', function ($subquery) use ($note_id) {
$subquery->select('patient_id')->from('notes')->where('id', $note_id)->limit(1);
})
->pluck('id');
// PHP >= 7.4
Note::where('id', '<>', $note_id)
->where('patient_id', fn($q) => $q->select('patient_id')->from('notes')->where('id', $note_id)->limit(1))
->pluck('id');
The later, you could turn into a query scope
# Note model
public function scopeSiblingsOf($query, $note_id)
{
return $query->where('id', '<>', $note_id)
->where('patient_id', function ($subquery) use ($note_id) {
$subquery->select('patient_id')
->from('notes')
->where('id', $note_id)
->limit(1);
});
}
# Usage
Note::siblingsOf(1)->pluck('id');

How to write sub queries in laravel 8?

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

How to fetch latest record of each user in a table using Laravel eloquent

I have n users in a table and for each user, I'm saving their log-in and log-out time in a separate table. I want to get the details of the users who haven't logged-in for 2 days using Laravel eloquent.
Users table structure
id | name | email
Log table structure
id |action | user_id | created_at | updated_at
So far I have done this much:
$users = LogsData::where('action','Login')->where('created_at','<',Carbon::now()->subDays(1))->get();
But the output has users who have logged-in within 2 days also.
EDIT:
I got the query, I need to convert this into eloquent.
I solved it myself. Here is the solution:
SELECT t1.* FROM actions t1
JOIN (SELECT user_id, MAX(id) as maxid FROM actions where action = "LOGIN" GROUP BY user_id) t2
ON t1.user_id = t2.user_id and t1.id = t2.maxid
where created_at < NOW() - INTERVAL 2 DAY
If you only need to get the last data of each user, you can sort the id desc and then group by user_id to get the latest data
$users = LogsData::where('action','Login')
->whereDate('created_at','<',Carbon::today()->subDays(1))
->orderBy('id', 'DESC')
->groupBy('user_id')
->get();
to use groupBy, you have to change the strict from your config value into false. But if you don't want to change your config file, this query can help you. You just need to translate it into laravel query version
SELECT * FROM log_datas AS ld WHERE ld.action = 'Login' AND ld.id IN (SELECT MAX(id) FROM log_datas WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 DAY) GROUP_BY user_id)
You need to make a join to the logs table first, and because MySQL always seeks the path of least resistance, you need to join it the other way around: there may not exists log entries YOUNGER than 1 day.
$users = DB::from('users')
->leftJoin('logs', function ($join) {
$join->on('users.id', '=', 'logs.user_id')
->where('logs.action', '=', 'Login')
->where('logs.created_at', '>=', Carbon::now()->subDays(2));
})
->whereNull('logs.id')
->get();
Maybe try using eloquent relations
Take note of your namespaces make sure that App\LogsData for example is correct here
// in your User Model
public function logsData()
{
return $this->hasMany('App\LogsData');
}
// in your LogsData Model
public function user()
{
return $this->belongsTo('App\User');
}
public function scopeLoginActions($query)
{
return $query->where('action', 'Login');
}
Then you can access data with
User::whereHas('logsData', function ($query) {
$query->loginActions()->where('created_at', '<', now()->subDays(2));
})->get();
// and if you require the login records
User::with('logsData')->whereHas('logsData', function ($query) {
$query->loginActions()->where('created_at', '<', now()->subDays(2));
})->get();
// and if you require an individual login record
User::with(['logsData' => function($query) {
$query->loginActions()->where('created_at', '<', now()->subDays(2))->first();
})->whereHas('logsData', function ($query) {
$query->loginActions()->where('created_at', '<', now()->subDays(2));
})->get();

How do I optimize this query with whereHas?

I was using this query to filter out stores with city and categories. It was working fine when I had around 1000 records in stores table. Now, when I have 5000 records it takes around 3-10 seconds to generate result.
A store belongs to multiple categories in my case.
How can I optimize this query using Eloquent orm or DB::raw()?
$stores = Store::where('city_id', $city_id)
->where('disabled', '0')
->whereHas('categories', function($q) use ($category_id){
$q->where('category_id', '=', $category_id);
})
->orderBy('rating','DESC')->paginate(10);
I solved my problem using whereRaw as DB::raw() or DB::select() can not paginate() the collection.
Problem:
Execution time: 11.449304103851s
city_id = 6 & $category_id = 1
$stores = Store::where('city_id', $city_id)
->where('disabled', '0')
->whereHas('categories', function($q) use ($category_id){
$q->where('category_id', '=', $category_id);
})
->orderBy('rating','DESC')->paginate(10);
Solution:
Execution time: 0.033660888671875s
city_id = 6 & $category_id = 1
$stores = Store::
where('city_id', $city_id)
->where('disabled', '0')
->whereRaw('stores.id in (select store_id from store_categories_pivot where category_id = ?)', [$category_id])
->orderBy('rating','DESC')
->paginate(10);
You could try running:
$stores = Store::where('city_id', $city_id)
->where('disabled', '0')
->leftJoin('categories','categories.store_id','=', 'stores.id')
->where('category_id', $category_id)
->orderBy('rating','DESC')->paginate(10);
and now verify your time execution. But you might need to add extra changes to such query because we don't know exact tables structure and how data is organized in them.
If it doesn't help you should get the query that is executed (exact query) and then run
EXPLAIN your_query
in Database Tool to show you what exactly is happening and whether do you really have indexes on everything that is needed.
Looking at your query you should probably have indexes for stores for columns:
city_id
disabled
rating
and for categories you should have indexes for columns:
category_id
store_id
or for some combinations of those columns.

Laravel orWhereHas not working as expected

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

Categories