I have a User entity with one-to-many relationship with Certificate entity (one user might have multiple certificates).
A Certificate has a startDate and endDate which determine their period of validity. I need filter all the users, that have expired certificates and have no active certificates.
An active certificate - c.startDate <= :today AND c.endDate > :today
An expired certificate - c.endDate <= :today.
I now how to do it in plain SQL:
SELECT
u.user_id
FROM `User` u JOIN `Certificate` c ON c.user_id = u.user_id
GROUP BY u.user_id
HAVING SUM(c.end_date > :today) = 0
Now I'm trying to port the SQL query to Doctrine QueryBuilder syntax:
$qb = $this->createQueryBuilder();
$qb
->from('SomeNS:User', 'u')
->select('u')
->join('u.certificates', 'c')
->groupBy('u.id')
->having('SUM(c.endDate > :today) = 0')
->setParameter('today', $today);
But an error occurs:
Error: Expected Doctrine\ORM\Query\Lexer::T_CLOSE_PARENTHESIS, got '<'
It looks like the conditions in the SUM function aren't understood by Doctrine.
Please help to figure out how one can use conditions in SUM function or point me out what I do wrong.
Here is a concrete answer using the expr() method of queryBuilder:
---
->having(
$qb->expr()->andx( // this is the "AND" in between the sums
$qb->expr()->lte( // this is the "<="
...(your SUM here, after you fix the logic),
0
),
$qb->expr()->eq( // this is the "="
...(your SUM here, after you fix the logic),
0
)
)
)
Once you get those "SUM" conditions right, you put them in the placeholders and it should work.
Related
I'm trying to query data from one table based on the SUMed and COALESCEd result of two associated tables.
The query works fine when executed as native SQL, unfortunately my application is restricted to only using Doctrine's DQL on a QueryBuilder object here.
This is how I build my QueryBuilder:
$resUnitQuery = $queryBuilder->getEntityManager()->createQueryBuilder();
$resUnitQuery->select('COALESCE(SUM(pc.residentialUnits), 0)')
->from(PropertyConnection::class, 'pc')
->where("$rootAlias.id = pc.contract");
$comUnitQuery = $queryBuilder->getEntityManager()->createQueryBuilder();
$comUnitQuery->select('COALESCE(SUM(pwa.residential_units), 0)')
->from(PropertyWithoutAddress::class, 'pwa')
->where("$rootAlias.id = pwa.contract");
$queryBuilder = $this->getEntityManager(Contract::class)->createQueryBuilder();
$queryBuilder
->select('id')
->from(Contract::class, 'o')
->andWhere(
':residentialUnits <= ' .
sprintf(
'((%s) + (%s))',
$resUnitQuery->getQuery()->getDQL(),
$comUnitQuery->getQuery()->getDQL()
)
)
->setParameter('residentialUnits', $params['rangeFrom']);
The generated DQL code is as follows:
SELECT id
FROM App\Entity\Contract o
WHERE :residentialUnits <= ((SELECT COALESCE(SUM(pc.residentialUnits), 0) FROM App\Entity\PropertyConnection pc WHERE o.id = pc.contract) + (SELECT COALESCE(SUM(pwa.residential_units), 0) FROM App\Entity\PropertyWithoutAddress pwa WHERE o.id = pwa.contract))
This results in the following error:
[Syntax Error] line 0, col 66: Error: Expected Literal, got 'SELECT'
I found an old Doctrine GitHub issue stating that double parenthetis can cause issues, which is why I skipped them in my WHERE clause and replaced
((%s) + (%s))
with
(%s) + (%s)
The surrounding parenthesis are required for native SQL to work, and it also results in an error from Doctrine:
[Syntax Error] line 0, col 174: Error: Expected end of string, got '+'
May I be running into restrictions of Doctrine's DQL here?
According to the docs, both SUM and COALESCE are supported, as well as nested queries.
Thanks a lot in advance,
Rene
Correct, the DBAL does not support arithmetic operations on subqueries. Additional the DBAL does not support joined subqueries, JOIN (SELECT FROM...) ON without using a native query.
Another issue is the WHERE statement of your subqueries, being dependant on the root query, will cause your root query to perform a full table scan. Executing each of the subqueries per-row, unless a criteria is added to the root query (WHERE o.id ...).
As the subquery SUM values are dependant on the root query id. You can rewrite the query to use a subquery as a hidden column and a HAVING clause, to filter the id result set from the added column results.
$em = $queryBuilder->getEntityManager();
$expr = $em->getExpressionBuilder();
$qbPC = $em->createQueryBuilder()
->select('COALESCE(SUM(pc.residentialUnits), 0)')
->from(App\Entity\PropertyConnection::class, 'pc')
->where($expr->eq('pc.contract', "$rootAlias.id"));
$qbPWA = $em->createQueryBuilder()
->select('COALESCE(SUM(pwa.residential_units), 0)')
->from(App\Entity\PropertyWithoutAddress::class, 'pwa')
->where($expr->eq('pwa.contract', "$rootAlias.id"));
$qb = $this->getEntityManager(Contract::class)
->createQueryBuilder()
->select('o.id')
->from(App\Entity\Contract::class, 'o')
->addSelect('(' . $qbPC->getDQL() . ') AS HIDDEN pc_ru')
->addSelect('(' . $qbPWA->getDQL() . ') AS HIDDEN pwa_ru')
->having($expr->lte(':v', 'pc_ru + pwa_ru'))
->setParameter('v', $params['rangeFrom']);
dump($qb->getDQL());
Resulting DQL
SELECT
o.id,
(SELECT
COALESCE(SUM(pc.residentialUnits), 0)
FROM App\Entity\PropertyConnection pc
WHERE pc.contract = o.id
) AS HIDDEN pc_ru,
(SELECT
COALESCE(SUM(pwa.residential_units), 0)
FROM App\Entity\PropertyWithoutAddress pwa
WHERE pwa.contract = o.id
) AS HIDDEN pwa_ru
FROM App\Entity\Contract o
HAVING :v <= pc_ru + pwa_ru
i have 2 tables
accounts : id , title , disabled , transaction_amount_limit , transaction_count_limit
account_limits : id , account_id , transaction_amount , transaction_count , date
so each account has bunch of transaction each day ... i want to select the a account that hasn't reached its transactions limit .... current transaction for each account is stored in account_limits table
basically i want to say select account that doesn't have account_limits row or have account_limits but hasn't reached the limits account_limits.transaction_amount < accounts.transaction_amount_limit && account_limits.transaction_count < accounts.transaction_count_limit
something like
select * from `accounts`
( where not exists (select * from `account_limits` where `accounts`.`id` = `account_limits`.`account_id`)
OR
where exists (select * from `account_limits` where `accounts`.`id` = `account_limits`.`account_id` && account_limits.transaction_amount < accounts.transaction_amount_limit && account_limits.transaction_count < accounts.transaction_count_limit)
)
i have this so far
$account = Account::where('disabled' , 0 )->where(function($q){
$q->whereDoesntHave('AccountLimit')->orWhere('....') ;
})->get();
as #Flame suggested i tried
Account::whereHas('accountLimits', function($query) {
$query->where('account_limits.transaction_amount', '<', 'accounts.transaction_amount_limit')
->where('account_limits.transaction_count', '<', 'accounts.transaction_count_limit');
})->orHas('accountLimits', '=', 0);
the problem is for some reason
where('account_limits.transaction_amount', '<', 'accounts.transaction_amount_limit')
in the output will translate to
where `account_limits`.`transaction_amount` < 'accounts.transaction_amount_limit'
and query fails , there's problem with quotations
this
'accounts.transaction_amount_limit'
should be
`accounts`.`transaction_amount_limit`
Here is an example answer in the Eloquent syntax. Note that you need to add the relation:
// Account.php , your eloquent model
public function accountLimits()
{
return $this->hasMany(AccountLimit::class);
}
And for the query:
Account::whereHas('accountLimits', function($query) {
$query->where('account_limits.transaction_amount', '<', 'accounts.transaction_amount_limit')
->where('account_limits.transaction_count', '<', 'accounts.transaction_count_limit');
})->orHas('accountLimits', '=', 0);
This checks for your where-clause in the relation using whereHas, and if it is not a match, it will also add the records that match in the orHas, which finds all Accounts without accountLimits relationships.
As a straight MySQL query, something like this should work:
SELECT a.*
FROM accounts a
JOIN limits l ON l.transaction_amount < a.transaction_amount_limit AND
l.transaction_count < a.transaction_count_limit
The JOIN condition will filter out any accounts that have met or exceeded either their transaction_amount or transaction_count limits.
It is good idea for me to keep transaction_count and transaction_limit in same table (accounts).
Then you can compare this columns.
$accounts = Account::whereRaw("transaction_count < transaction_limit)->get();
How can I convert below query to Doctrine:
SELECT restaurants.restaurant_name ,
restaurants.restaurant_id,
j.LASTPRICE
FROM restaurants
LEFT JOIN
(
SELECT f.food_id AS fid,
f.restaurants_restaurant_id AS rid,
Max(f.food_last_price) AS LASTPRICE
FROM foods AS f
LEFT JOIN restaurants AS r
ON r.restaurant_id = f.restaurants_restaurant_id
WHERE f.food_last_price IS NOT NULL
GROUP BY r.restaurant_id) j
ON restaurants.restaurant_id = j.rid
Here is my code:
$qb = $this->_em->createQueryBuilder();
$qb2 = $this->_em->createQueryBuilder();
$getMaxPercentage = $qb2
->select(
'MAX (Food.foodLastPrice) AS LASTPRICE ',
'Food.foodId AS fId',
'Food.restaurantsRestaurantId AS rID'
)
->from($this->entityClass,'Restaurant')
->innerJoin('Restaurant.foods','Food')
->where('Food.foodLastPrice IS NOT NULL')
->groupBy('Restaurant.restaurantId')
->getDQL();
$restaurantList = $qb
->select('Restaurants.restaurantName, Restaurants.restaurantId , j.LASTPRICE')
->from($this->entityClass,'Restaurants')
->leftJoin($getMaxPercentage,'j','WITH','Restaurants.restaurantId = j.rID')
->getQuery()->execute();
dd($restaurantList);
I give an error :
SELECT Restaurants.restaurantName,': Error: Class 'SELECT' is not defined.
I've already known I could set sub queries in main query, Although in this case I does not want to use sub query in Where expression. Any suggestion for using select in LeftJoin in doctrine?
EDITED : I've tried to use DQL in my query:
$query = $this->_em->createQuery(
'
SELECT Restaurants.restaurantName , Restaurants.restaurantId
FROM App\\Restaurant AS Restaurants
LEFT JOIN (
SELECT f.foodId AS fid,
f.restaurantsRestaurantId AS rid,
Max(f.foodLastPrice) AS LASTPRICE
FROM App\\Food AS f
LEFT JOIN App\\Restaurant AS r
WITH r.restaurantId = f.restaurantsRestaurantId
GROUP BY r.restaurantId) AS J
ON Restaurants.restaurantId = j.rid
');
But I gave an another error :
[Semantical Error] Error: Class '(' is not defined.
Is it possible to use select in left join in Doctrine?
EDITED 2 : I read a similar question and I've decided to write in another way :
$qb = $this->_em->createQueryBuilder();
$qb2 = $this->_em->createQueryBuilder();
$subSelect = $qb2
->select(
array(
'Food.foodLastPrice AS LASTPRICE ',
'Food.foodId AS fId',
'Food.restaurantsRestaurantId AS rId')
)
->from($this->entityClass,'Restaurant')
->innerJoin('Restaurant.foods','Food')
->where('Food.foodLastPrice IS NOT NULL')
->groupBy('Restaurant.restaurantId')
->getQuery()->getSQL();
$restaurantList =
$qb->select(
'Restaurant1'
)
->from($this->entityClass, 'Restaurant1')
->leftJoin('Restaurant1',sprintf('(%s)',$subSelect),'internalQuery','Restaurant1.restaurantId = internalQuery.rId')
->getQuery()->getSQL();
dd($restaurantList);
Again, I got an error:
near 'Restaurant1 (SELECT': Error: Class 'Restaurant1' is not defined.
What you're trying to do is impossible :
https://github.com/doctrine/doctrine2/issues/3542 (november 2013, but doctrine concept didn't change since and doctrinebot closed this on 7 Dec 2015)
DQL is about querying objects. Supporting subselects in the FROM
clause means that the DQL parser is not able to build the result set
mapping anymore (as the fields returned by the subquery may not match
the object anymore). This is why it cannot be supported (supporting it
only for the case you run the query without the hydration is a no-go
IMO as it would mean that the query parsing needs to be dependant of
the execution mode).
In your case, the best solution is probably to run a SQL query instead
(as you are getting a scalar, you don't need the ORM hydration anyway)
You can find workarounds like raw sql, or rethinking your query.
I got a query that works fine in PMA and I'm trying to convert it to Eloquent, but I must be missing something.
Here the SQL query:
SELECT t1.* FROM ppr__child_absence t1
LEFT JOIN ppr__children t3 ON t3.idcode=t1.child_idcode
WHERE t1.id = (SELECT t2.id FROM ppr__child_absence t2
WHERE t2.child_idcode = t1.child_idcode
ORDER BY t2.id DESC LIMIT 1)
AND t3.deleted_at IS NULL
AND $input BETWEEN start AND stop;
and hes is my eloquent query:
$input = Request::all();
$pprs =
DB::table('ppr__child_absence')
->join('ppr__children', function($join) {
$join->on('ppr__children.idcode', '=', 'ppr_children_absence.child_idcode')
->where('ppr__child_absence.child_idcode', '=', DB::raw('SELECT t2.id FROM ppr__child_absence t2
WHERE t2.child_idcode = ppr__child_absence.child_idcode
ORDER BY t2.id DESC LIMIT 1'));})
->whereNull('ppr__children.deleted_at')
->whereBetween($input, array('ppr_children_absence.start','ppr_children_absence.stop'))->get();
I'm keep getting error:
ErrorException in Grammar.php line 58:
strtolower() expects parameter 1 to be string, array given.
Can anyone here lead me to right direction?
Basically user has 1 date input with datepicker and submit button to get result according to the date chosen.
working query here with out errors but no results
$input = Request::all();
$pprs = DB::table('ppr__child_absence')
->select('ppr__child_absence.*')
->join('ppr__children', function($join) {
$join->on('ppr__children.idcode', '=', 'ppr__child_absence.child_idcode')
->where('ppr__child_absence.id', '=', DB::raw('SELECT t2.id FROM ppr__child_absence t2
WHERE t2.child_idcode = ppr__child_absence.child_idcode
ORDER BY t2.id DESC LIMIT 1'));})
->whereNull('ppr__children.deleted_at')
->where('ppr__child_absence.start', '<', $input['ppr_day'])
->where('ppr__child_absence.stop', '>', $input['ppr_day'])->get();
$input = Request::all(); stores all the input fields in an array.
You are using ->whereBetween($input,...) and the first parameter of whereBetween() is expected to be a string (the name of the table column) but you are passing an array.
You didn't give details about the input. Assuming the input contains a field called 'field', the you should use ->whereBetween($input['field'],...) instead
What you seem to want is not what the whereBetween method offers. When using whereBetween the logic is that you want a given column value to be between two input values. The example in the Laravel Query Builder Documentation is chosen well:
// the column 'votes' value should be between 1 and 100
$users = DB::table('users')->whereBetween('votes', [1, 100]);
What you seem to want is to make sure an input value is between two different column values. You can do that by having two separate conditions:
->where('ppr_children_absence.start', '<', $input['value'])
->where('ppr_children_absence.stop', '>', $input['value'])
This will make sure that $input['value']) is between the start and stop column values.
I have two tables related to each other (main_table OneToMay detail_table). There is a deadline_days field in main table and a create_date in detail_table. I want to select all details which create_date+main.deadline_days are passed base on today date. (This was the scenario)
This is the proper MySQL query which gives me right records
SELECT `D`.* FROM `details_table` AS `D`
INNER JOIN `main_table` AS `M` ON (`D`.`Main_Id` = `M`.`id`)
WHERE DATE_ADD(`D`.`Create_Date`, INTERVAL `M`.`Deadline_days` DAY) <= NOW()
Now in Symfony when I want to create the query using createQueryBuilder it comes with this error
[Syntax Error] line 0, col 165: Error: Expected Doctrine\ORM\Query\Lexer::T_COMMA, got 'M'
This is what I have for now in my query builder
$result = $this->createQueryBuilder('D')
->join('D.Main', 'M')
->where('DATE_ADD(D.Create_Date, INTERVAL M.DeadLine_Days DAY) <= NOW()')
->getQuery()
->getResult();
Any idea how to fix this?
Thanks in advance.
Please do not suggest using native query
This is what I found base on this link (Doctrine Update DQL function signature)
Doctrine2 has function DATE_ADD but does not have INTERVAL param as MySQL, also the unit param should be as string 'DAY'; so the Query should be:
$result = $this->createQueryBuilder('D')
->join('D.Main', 'M')
->where('DATE_ADD(D.Create_Date, M.DeadLine_Days, 'DAY') <= CURRENT_DATE()')
->getQuery()
->getResult();
In doctrine we should use CURRENT_DATE() instead of NOW()