Add extra SELECT expression result to the existing entity - php

I have the project that allows to rate a company in many aspects. The following diagram could help you to understand the relations in my database. Of course, I have deleted from the scheme the fields, that are not important in the context of my problem.
Currently, in my repository class of the Company entity, I am searching for companies using QueryBuilder and returning an instance of \Doctrine\ORM\Tools\Pagination\Paginator.
The below code is responsible for fetching companies:
public function search(CompanySearchQuery $command): Paginator
{
$qb = $this->createQueryBuilder('c')
->setFirstResult($command->getOffset())
->setMaxResults($command->getMaxResults())
->orderBy('c.name', 'ASC');
... plus extra "where" conditions
return new Paginator($qb, true);
}
I wonder how I can attach the average score to each company using QueryBuilder and Paginator.
Here is the native SQL that allows me to get data I need:
SELECT
c.id,
c.name,
AVG(o2.score) as score
FROM
company c
LEFT JOIN
opinion o ON c.id = o.company_id
LEFT JOIN
opinion_scope o2 ON o.id = o2.opinion_id
GROUP BY
c.id
My question is: Is it possible to add averageScore property to the Company class and map it from QueryBuilder result?
I tried to rewrite my SQL query to use with the existing code:
$qb = $this->createQueryBuilder('c')
->select('c', 'AVG(s.score)')
->leftJoin('c.opinions', 'o')
->leftJoin('o.scopes', 's')
->groupBy('c.id')
->setFirstResult($command->getOffset())
->setMaxResults($command->getMaxResults())
->orderBy('c.name', 'ASC')
;
With the above code I get database exception as following:
SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #14 of
SELECT list is not in GROUP BY clause and contains nonaggregated column
'database.o1_.score' which is not functionally dependent on columns in GROUP BY
clause; this is incompatible with sql_mode=only_full_group_by").
The SQL query that is executed for the above QueryBuilder is:
SELECT
DISTINCT id_8
FROM
(
SELECT
DISTINCT id_8,
name_9
FROM
(
SELECT
c0_.updated_at AS updated_at_0,
c0_.description AS description_1,
c0_.website AS website_2,
c0_.email AS email_3,
c0_.phone AS phone_4,
c0_.street AS street_5,
c0_.postal_code AS postal_code_6,
c0_.city AS city_7,
c0_.id AS id_8,
c0_.name AS name_9,
c0_.slug AS slug_10,
c0_.created_at AS created_at_11,
AVG(o1_.score) AS sclr_12,
o1_.score AS score_13,
o1_.opinion_id AS opinion_id_14,
o1_.type_id AS type_id_15
FROM
company c0_
LEFT JOIN opinion o2_ ON c0_.id = o2_.company_id
LEFT JOIN opinion_scope o1_ ON o2_.id = o1_.opinion_id
GROUP BY
c0_.id
) dctrn_result_inner
ORDER BY
name_9 ASC
) dctrn_result
LIMIT
2 OFFSET 0
I do not understand why Doctrine adds the following fragment:
o1_.score AS score_13,
o1_.opinion_id AS opinion_id_14,
o1_.type_id AS type_id_15

The issue seems to be that you're trying to select every column from 'c', which is your resulting table from performing two LEFT JOINs. Instead, you need to be specifying a from clause for the company table:
$qb = $this->createQueryBuilder()
->select('c', 'AVG(s.score)')
->from('company', 'c')
->leftJoin('c.opinions', 'o')
->leftJoin('o.scopes', 's')
->groupBy('c.id')
->setFirstResult($command->getOffset())
->setMaxResults($command->getMaxResults())
->orderBy('c.name', 'ASC')
;
This will avoid including o1.score is your returned columns, which is what that error is referencing

Related

How to convert raw subquery into laravel query with query builder?

I have written a raw query which also contains subquery. I'm getting no idea that how to convert subquery in the laravel subquery with query builder.
Please someone convert this query. It will be very appreciable.
Query:
SELECT inventory.EmployeeID,
inventory.created_date AS OrderDate,
SUM(inventory.calculation) AS TotalPrice
FROM ( SELECT i.id AS ItemID,
o.id AS OrderID,
o.EmployeeID,
o.created_date,
(o.Quantity * i.price) AS calculation
FROM `stationary_orders` AS o
LEFT JOIN `stationary_items` AS i ON o.Stationary_ID = i.id
WHERE o.Store IN $storess
ORDER BY o.id DESC
LIMIT $Limit,10 ) AS inventory
GROUP BY inventory.EmployeeID
It took me a while to understand what's going on.
As I understood from your raw query, this is called a nested query.
You can use DB facade to make this query like this:
DB::select('inventory.EmployeeID, inventory.created_date AS OrderDate, SUM(inventory.calculation) AS TotalPrice')
->fromSub(function ($nestedQuery) use ($stores) {
$nesterQuery->select('i.id ...')->from('stationary_orders as o')
->leftJoin('stationary_items', 'o.Stationary_ID', '=', 'stationary_items.id')
->whereIn('o.Store', $stores)
...
}, 'inventory')
->groupBy('inventory.EmployeeID')->get()

How to join with DB::table and orderBy jointable attribute set 0 if no join found

I have a DB::table query that I make a join with. In case the join is not possible, I want to set the value of the attribute that I'm looking for with my join to 0 for the order. With my current query, entries that have no join are ignored.
$d_list = DB::table('d')->where('active', 1)
->join('d_scores', 'd_id', '=', 'd.id')
->orderBy(DB::raw('(case when d_scores.totalscore is null then 0 else d_scores.totalscore end)'), 'desc')
->orderBy('u_24h', 'desc')
->orderBy('d.nf', 'desc')
->orderBy('d.id', 'desc')
->get(array('d.id', 'd_scores.totalscore'));
foreach($d_list as $key => $d){
if($d->id == $data['d']->id){
$d_rank = $key+1;
break;
}
}
I have a table with 1000 entries. Out of these 1000, only 100 have a row in the join table. I want to order the result of 1000 based on the value of the attribute that only 100 have though. The 900 other entries that don't have a row in the join table need to get the value 0 for that specific attribute that doesn't exist for them (d_scores.totalscore). But because the join doesn't exist for them, these entries are not taken anymore at all.
SELECT A.id,IFNULL(B.field_that_might_be_matched,0)
FROM table_a A
LEFT JOIN
table_b B
ON
A.id = B.table_a_id
ORDER BY 2
The LEFT JOIN will make sure the result will show all entries in table A, regardless if they have a match in table B.
If there is no match in table B, it means there is no value for B.field_that_might_be_matched in the result. By default, mysql will put there NULL.
The ISNULL condition will turn it from NULL to 0.
ORDER BY 2, order it by the second field in the select list (Check me out here, been some time since I used this syntax).
Sadly, I do not support this pseudo MySQL in PHP, which seems smart, but actually, isn't. So the example is in pure SQL. I think it is easy to translate to the "ORM" u use.

How to select from a table based on another table's values Eloquent?

I am trying to get the data on some students that are still active. Even tho I have data from inactive students in the same table.
This is the StudentAttendance
This is the StudentClass
This is the Eloquent query that I came up with:
StudentAttendance::
select('student_classes.active', 'student_attendances.student_id', 'student_attendances.classroom_id', 'classrooms.classroom', 'attendance_rules.option')
->join('classrooms', 'classrooms.id', '=', 'student_attendances.classroom_id')
->join('attendance_rules','attendance_rules.id', '=', 'student_attendances.attendance_id')
->join('student_classes', 'student_attendances.student_id', '=', 'student_classes.student_id')
->where('attendance_date', date("Y-m-d"))
->orderBy('classrooms.classroom', 'ASC')
->get();
SQL:
select `student_classes`.`active`, `student_attendances`.`student_id`, `student_attendances`.`classroom_id`, `classrooms`.`classroom`, `attendance_rules`.`option`
from `student_attendances`
inner join `classrooms` on `classrooms`.`id` = `student_attendances`.`classroom_id`
inner join `attendance_rules` on `attendance_rules`.`id` = `student_attendances`.`attendance_id`
inner join `student_classes` on `student_attendances`.`student_id` = `student_classes`.`student_id`
where `attendance_date` = '2020-02-11'
order by `classrooms`.`classroom` asc
Now my Eloquent query results into this:
As you can see the student_id 22 with the classroom_id of 2 is inactive but it appears to be inactive once and the rest active. If I remove the student_classes join I won't get all the repeated results.
The goal is to display all the attendances of today where the student is active (active=1) in the StudentClass even if I query in the StudentAttendance.
You will want to scope your join to student_classes to only look at active records.
You can do this by using a callback in your join method:
->join('student_classes', function ($join) {
$join->on('student_attendances.student_id', '=', 'student_classes.student_id')
->on('student_classes.classroom_id', '=', 'student_attendances.classroom_id')
->where('student_classes.active', 1);
})
This is covered under the 'Advanced Join Clauses' in the Query Builder docs - https://laravel.com/docs/5.8/queries#joins

Wrong SQL query generated by Doctrine2 Paginator

My scripts stop working after I've upgraded from Doctrine 2.3.6 up to Doctrine 2.5.0 .
I want to perform query with pagination on joined tables. I have two entities: Program and ProgramVersion joined by Program::versions field.
Here is the query:
$query = $em->createQuery('
SELECT m, v
FROM Entity\Program m
LEFT JOIN m.versions v
WITH m.version = v.version
WHERE m.deletedDate IS NULL
ORDER BY m.type asc');
Then I pass the query to Paginator
$pResult = new \Doctrine\ORM\Tools\Pagination\Paginator($query, true);
At the stage when I do $pResult->getIterator() I get the following error message:
An exception occurred while executing '
SELECT DISTINCT id_0
FROM (
SELECT e0_.id AS id_0, e1_.id AS id_27 /* removed a lot of fields here */ FROM entity_program e0_
LEFT JOIN entity_program_version e1_ ON e0_.id = e1_.master_id AND (e0_.version = e1_.version)
WHERE e0_.deleted_date IS NULL
) dctrn_result ORDER BY e0_.type_id ASC':
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'e0_.type_id' in 'order clause'
Obviously the SQL is wrong. The ORDER BY clause is outside of the main SELECT and it contains the field which doesn't exist.
Can anybody help me to solve the issue?

How to order grouped results by last id using Eloquent and MySQL?

I tried this query but it only order id column, rest is not.
$chapters = Test::select(DB::raw('*, max(id) as id'))
->groupBy('id_equip')
->orderBy('id', 'asc')
->get();
In MySQL when using group by you can't rely on order by clause (it won't work as you expect, ie. it will not order the results in groups, but rather return random row from the group).
So in order to achieve what you want, you need a subquery or join:
// assuming tests table, group by id_equip, order by id
SELECT * FROM tests WHERE id = (
SELECT MAX(id) FROM tests as t WHERE t.id_equip = tests.id_equip
) ORDER BY id
SELECT * FROM tests
JOIN (SELECT MAX(id) as id FROM tests ORDER BY id DESC) as sub
ON sub.id = tests.id
This will get the highest id for each id_equip and return whole row for each of them.
Now, in eloquent I suggest first approach, if you want it to look more intuitive:
Test::where('id', function ($sub) {
// subquery
$sub->selectRaw('max(id)')
->from('tests as t')
->where('t.id_equip', DB::raw('tests.id_equip'));
// order by
})->orderBy('id', 'desc')->get();
but 2nd appreach is probably the way if you have big table to scan (in terms of performance):
Test::join( DB::raw(
'(select max(id) as id from tests group by id_equip order by id desc) sub'
), 'sub.id', '=', 'posts.id')
->get(['tests.*']);
Here you need to set order by clause inside the raw join statement.
You could also build that join subquery with the builder if you like:
$sub = Test::selectRaw('max(id)')
->groupBy('id_equip')
->orderBy('id', 'desc')
->toSql();
Test::join( DB::raw(
"({$sub}) as sub"
), 'sub.id', '=', 'posts.id')
->get(['tests.*']);

Categories