Laravel group by on model hierarchy - laravel

I've searched this about two days in the site but was unable to find a similar example.
I'm trying to code the equivalent of this Oracle command in Eloquent:
select d.code, d.grp, d.cname, sum(d.poso1)
from pers_data e, misth_master m, misth_detail d
where e.per_emploeeno = m.emp_no
and m.misaa = d.misaa
and e.per_emploeeno = '04004'
and m.year = '2019'
group by d.code, d.grp, d.cname;
It basically joins 3 models in hierarchical fashion ( e -(has_many)-> m -(has_many)-> d), applies constraints on 1st and 2nd and groups/sums data from the 3rd.
The result set is smt like this:
CODE GRP CNAME SUM(POSO1)
---- --- ----- ----------
153 1 cname1 0
2 0 cname2 3480
2 1 .. 1003,2
162 1 .. 250,8
5 .. 2464,67
30 4 .. 172,68
102 1 .. 949,08
105 1 .. 37,5
111 1 .. 25
0 0 .. 21600
11 1 cnamen 3976,26
In Laravel, I've declared the relevant models (e=Employee with relationships to m=misthoi and d=details).
After many experiments, I've come to this lines of code to achieve this:
(Please note that Employee model has a global scope and thus needs not be constrained in this piece of code, i.e. e.per_emploeeno = '04004' is implied)
$mdata_flat = Employee::with(['misthoi' => function ($query) {
$query->where('year', '2019');
}, 'misthoi.details'])->first()->misthoi->pluck('details')->flatten();
$mdata_grouped = $mdata_flat->groupBy(function ($item, $key) {
return $item['grp'].'.'.$item['code'];
})->map(function ($item) {
return [$item->max('cname'), $item->sum('poso1')];
})->toArray();
It works, indeed, but seems to me complicated and I'm fan of simple, readable code.
I'm convinced there is a neat way to do this with Eloquent.
Could it be written in simpler manner?
It could be generalized as:
Let A, B, C be 3 hierarchical related models (A->B->C).
I want to group data of C only, based on constraints on A and B.
Thank you in advance!

I don't 100% understand the Oracle syntax, but it looks like joins to me.
I would "translate" it to Laravel's query builder as:
DB::table('pers_data')
->join('misth_master', 'pers_data.per_emploeeno', '=', 'misth_master.emp_no')
->join('misth_detail', 'misth_master.misaa', '=', 'misth_detail.misaa')
->where('pers_data.per_emploeeno', '=', '04004')
->where('misth_master.year', '=', '2019')
->select(DB::raw('SUM(misth_detail.poso1) as total'))
->groupBy('misth_detail.code', 'misth_detail.grp', 'misth_detail.cname')

Related

Laravel: Sum column based on relationship condition

Let's say we have two tables:
Payments
id
reason_id
amount
1
1
100
2
2
10
3
1
30
Payment Reasons
id
title
is_discount
1
Payment for something
0
2
Discount for something
1
I'd like to query payments table and sum amount column based on its relationship payment_reasons.is_discount value:
total_received
total_discounted
130
10
How to achieve this?
If you want to get the data in one db-request, I don't think there is a clean laravel-way to solve this. I've been trying out some raw mysql to get it into a working query and it would look like this:
select
sum(if(pr.is_discount = 0, p.amount, 0)) as total_received,
sum(if(pr.is_discount = 1, p.amount, 0)) as total_discounted
from payments p
join payment_reasons pr on pr.id = p.reason_id
The if statement will render if discount needs to be used per row and the sum wil simply sum all the end-results together.
Doing this in laravel with a model does not really make sence, since there are no fields in the result which your model could use anyway. In this case, using eloquent would be more (unmaintainable) code than a simple Query Builder:
$data = DB::select('
select
sum(if(pr.is_discount = 0, p.amount, 0)) as total_received,
sum(if(pr.is_discount = 1, p.amount, 0)) as total_discounted
from payments p
join payment_reasons pr on pr.id = p.reason_id
')
If you don't mind doing multiple queries to fetch the data, models make more sense:
$totalReceived = Payment
::whereHas('paymentReason', fn($q) => $q->where('is_discount', 0))
->sum('amount');
$totalDiscounted = Payment
::whereHas('paymentReason', fn($q) => $q->where('is_discount', 1))
->sum('amount');
Please note that this example will perform 4 queries, 2 on payments, and 2 on payment_reasons

Laravel/Eloquent whereIn/where with a pair of sets

I have 3 tables in my database.
The first two tables are just normal tables with an ID and some other columns like:
Table 1
ID
col01
1
...
2
...
Table 2
ID
col01
1
...
2
...
The third table is some kind of a relation/assignment table:
Table 3
ID
table1_id
table2_id
text
1
1
1
..
2
1
2
..
3
1
3
..
4
2
1
..
5
3
3
..
Now I do have a SQL statement which does exactly what I want:
SELECT * FROM table_3 where (table1_id, table2_id) in ( (1, 1), (2, 1), (3, 3));
So Im sending following Request Body to the API:
{
"assignments": [
{
"table1_id": 1,
"table2_id": 1
},
{
"table1_id": 2,
"table2_id": 1
},
{
"table1_id": 3,
"table2_id": 3
}
]
}
I do validate my the request with
->validate($request,
[
'assignments' => 'required|array',
'assignments.*.table1_id' => 'required|integer|min:1|max:20',
'assignments.*.table2_id' => 'required|integer|min:1|max:20'
]
Now Im kinda stuck how to use the eloquent commands (e.g. whereIn) to get my desired output.
Thanks in advance!
EDIT
So I took the workaround of arcanedev-maroc mentioned here: https://github.com/laravel/ideas/issues/1021
and edited it to fit my Request.
Works like a charm.
Laravel does not provide any functions by default. The core team said that they would not maintain this feature. You can read the post here.
But you can create your own query to accomplish this. I am providing a function that you can use as per your specification:
public function test(Request $request)
{
$body=$request->input('data');
$data=json_decode($body)->assignments;
$query='(table1_id, table2_id) in (';
$param=array();
foreach($data as $datum)
{
$query=$query."(".$datum->table1_id.",".$datum->table2_id."), ";
}
$query = rtrim($query, ", ");
$query = $query.")";
$result=DB::table('table3')->whereRaw($query)->get();
return $result;
}
So I took the workaround of arcanedev-maroc mentioned here: https://github.com/laravel/ideas/issues/1021
and edited it to fit my Request.
Works like a charm.

Count the number of replies from a comment in a given post using laravel eloquent

I have a sample comments table below:
id | parent_id | post_id
1 0 1
2 0 1
3 1 1
4 2 1
5 1 1
What I am trying to achieve is to get all comments (parent_id=0) based on post_id, and count the total replies at the same time. When a query is executed it should display like the result below:
id 1 has 2 replies
id 2 has 1 reply
Here is my sample query below and it gets all comments from the given post but the problem is, I am not sure how to count at the same time in one query.
Comment::where('parent_id', '=', 0)
->where('post_id', $postId)
->get();
Does anybody know how to fix this?
You can define a method in your Comment model class.
As follows:
public function replies()
{
return $this->hasMany('App\Models\Comment','parent_id');
}
Then you can get the number of replies using the following code:
$comments = Comment::where('parent_id', '=', 0)
->where('post_id', $postId)
->withCount('replies')
->get();
In this case, you can access the number of comments with the following code:
foreach ($comments as $comment){
$countOfReplies = $comment->replies_count;
}
I hope it helps

Wrong total pagination with select distinct in Laravel 6

I use Laravel 6.12, I have this request :
$queryJob = DB::table('jobs as j')->join('job_translations as jt', 'j.id', 'jt.job_id')
->whereNull('j.deleted_at')
->whereNull('jt.deleted_at')
->select('j.id', 'j.short_name', 'j.status', DB::raw("case when j.short_name = '{$request->short_name}' then 0 else 1 end"))
->distinct();
$jobs = $queryJob->paginate($qtyItemsPerPage);
The results displays an error for the total :
The total = 3, but as you can see the data contains only 2 elements.
I read here that when using a distinct, I must be clear on which column the total must be calculated: distinct() with pagination() in laravel 5.2 not working
So I modified my query like that:
$jobs = $queryJob->paginate($qtyItemsPerPage, ['j.*']);
But without success, the total is still wrong.
Hoping that I don't misunderstand your DB and relations structure and purpose of your query perhaps this will avoid using distinct or groupBy altogether?
$shortname = $request->input('short_name');
$queryJob = Job::with('job_translations')->select('id','short_name',
'status', DB::raw("case when short_name = '" . $shortname . "'
then 0 else 1 end")
->paginate($qtyItemsPerPage);
Pagination can be easily manually added with skip and take in case you need to use groupBy
$queryJob->skip(($page - 1) * $qtyItemsPerPage)->take($qtyItemsPerPage)->get();
The solution for me was to pass a field name to the distinct() method.
With your example:
$queryJob = DB::table('jobs as j')
// joins, where and other chained methods go here
->distinct('j.id')
Solution taken from https://stackoverflow.com/a/69073801/3503615

How to search by laravel Eloquent Many to Many

three tables
user
id name
1 a
2 b
3 c
role
id name
1 aa
2 bb
3 cc
4 dd
role_user
role_id user_id
1 1
2 3
2 1
3 4
3 3
3 2
3 1
Laravel Code:
User::with('roles')->whereHas('roles', function($query) {
$query->whereIn('role_id', [2, 4]);
})->get();
this query return user has role 2 or 4,
I want get user has role 2 and 4;
what can I do?
To achieve what you're after you can use havingRaw():
User::with('roles')->whereHas('roles', function ($query) {
$query
->select('user_id')
->whereIn('role_id', [2, 4])
->groupBy('user_id')
->havingRaw('count(distinct(role_id))=2'); // 2 = The number of different roles i.e. count([2,4])
})->get();
When using whereIn you're essentially asking for retsults that include at least one of the values you pass to it. Using having in conjunction with group by allows you to stipulate that you want to include a certain number of them (in this case all of them).

Resources