How can I order by NULL in DQL? - symfony

I'm building an app using Symfony2 framework and using Doctrine ORM. I have a table with airlines for which some IATA codes are missing. I'm outputting a list, ordered by this IATA code, but I'm getting the undesirable result that the records with null IATA codes are sorted at the top.
In MySQL this is simple enough to do, with ORDER BY ISNULL(code_iata), code_iata but I'm clueless as to what the equivalent would be for DQL. I tried
$er->createQueryBuilder('airline')->orderBy('ISNULL(airline.codeIata), airline.codeIata', 'ASC')
but this gives me a syntax error.
The Doctrine docs give me no answer either. Is there a way?

You can use the following trick in DQL to order NULL values last
$em->createQuery("SELECT c, -c.weight AS HIDDEN inverseWeight FROM Entity\Car c ORDER BY inverseWeight DESC");
The HIDDEN keyword (available since Doctrine 2.2) will result in omitting the inverseWeight field from the result set and thus preventing undesirable mixed results.
(The sort fields value is inverted therefore the order has to be inverted too, that's why the query uses DESC order, not ASC.)
Credits belong to this answer.

The most unobtrusive generic solution would be to use the CASE expression in combination with the HIDDEN keyword.
SELECT e,
CASE WHEN e.field IS NULL THEN 1 ELSE 0 END HIDDEN _isFieldNull
FROM FooBundle:Entity e
ORDER BY _isFieldNull ASC
Works with both numeric as well as other field types and doesn't require extending Doctrine.

If you want to do something similar to "NULLS LAST" in SQL (with PostgreSQL in my case):
ORDER BY freq DESC NULLS LAST
You can use the COALESCE function with the Doctrine Query Builder
(HIDDEN will hide the field "freq" on your query result set).
$qb = $this->createQueryBuilder('d')
->addSelect('COALESCE(d.freq, 0) AS HIDDEN freq')
->orderBy('freq', 'DESC')
->setMaxResults(20);

Here it is an example for a custom walker to get exactly what you want. I have taken it from Doctrine in its github issues:
https://github.com/doctrine/doctrine2/pull/100
But the code as it is there didn't work for me in MySQL. I have modified it to work in MySQL, but I haven't test at all for other engines.
Put following walker class for example in YourNS\Doctrine\Waler\ directory;
<?php
namespace YourNS\Doctrine\Walker;
use Doctrine\ORM\Query\SqlWalker;
class SortableNullsWalker extends SqlWalker
{
const NULLS_FIRST = 'NULLS FIRST';
const NULLS_LAST = 'NULLS LAST';
public function walkOrderByClause($orderByClause)
{
$sql = parent::walkOrderByClause($orderByClause);
if ($nullFields = $this->getQuery()->getHint('SortableNullsWalker.fields'))
{
if (is_array($nullFields))
{
$platform = $this->getConnection()->getDatabasePlatform()->getName();
switch ($platform)
{
case 'mysql':
// for mysql the nulls last is represented with - before the field name
foreach ($nullFields as $field => $sorting)
{
/**
* NULLs are considered lower than any non-NULL value,
* except if a – (minus) character is added before
* the column name and ASC is changed to DESC, or DESC to ASC;
* this minus-before-column-name feature seems undocumented.
*/
if ('NULLS LAST' === $sorting)
{
$sql = preg_replace_callback('/ORDER BY (.+)'.'('.$field.') (ASC|DESC)/i', function($matches) {
if ($matches[3] === 'ASC') {
$order = 'DESC';
} elseif ($matches[3] === 'DESC') {
$order = 'ASC';
}
return ('ORDER BY -'.$matches[1].$matches[2].' '.$order);
}, $sql);
}
}
break;
case 'oracle':
case 'postgresql':
foreach ($nullFields as $field => $sorting)
{
$sql = preg_replace('/(\.' . $field . ') (ASC|DESC)?\s*/i', "$1 $2 " . $sorting, $sql);
}
break;
default:
// I don't know for other supported platforms.
break;
}
}
}
return $sql;
}
}
Then:
use YourNS\Doctrine\Walker\SortableNullsWalker;
use Doctrine\ORM\Query;
[...]
$qb = $em->getRepository('YourNS:YourEntity')->createQueryBuilder('e');
$qb
->orderBy('e.orderField')
;
$entities = $qb->getQuery()
->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, '\YourNS\Doctrine\Walker\SortableNullsWalker')
->setHint('SortableNullsWalker.fields', array(
'sortOrder' => SortableNullsWalker::NULLS_LAST
))
->getResult();

DQL does not contain every function of plain SQL. Fortunately you can define your custom DQL method to accomplish this.
Some resources:
http://punkave.com/window/2012/07/24/for-the-php-crowd-adding-custom-functions-to-doctrine-2-dql
http://docs.doctrine-project.org/en/2.1/cookbook/dql-user-defined-functions.html
http://symfony.com/doc/2.0/cookbook/doctrine/custom_dql_functions.html

By default, MySQL will still sort a NULL value; it will just place it at the beginning of the result set if it was sorted ASC, and at the end if it was sorted DESC. Here, you're looking to sort ASC, but you want the NULL values to be at the bottom.
Unfortunately, as powerful as it is, Doctrine isn't going to offer much support here, since function support is limited, and most of it is limited to SELECT, WHERE, and HAVING clauses. You actually wouldn't have a problem at all if any of the following were true about the QueryBuilder:
select() accepted ISNULL()
orderBy() or addOrderBy() supported ISNULL()
the class supported the concept of UNIONs (with this, you could run two queries: one where the codeIata was NULL, and one where it wasn't, and you could sort each independently)
So that said, you can go with the user-defined functions that ArtWorkAD mentioned already, or you could replicate that last point with two different Doctrine queries:
$airlinesWithCode = $er->createQueryBuilder("airline")
->where("airline.iataCode IS NULL")
->getQuery()
->getResult();
$airlinesWithoutCode = $er->createQueryBuilder("airline")
->where("airline.iataCode IS NOT NULL")
->getQuery()
->getResult();
Then you can combine these into a single array, or treat them independently in your templates.
Another idea is to have DQL return everything in one data set, and let PHP do the heavy lifting. Something like:
$airlines = $er->findAll();
$sortedAirlines = array();
// Add non-NULL values to the end if the sorted array
foreach ($airlines as $airline)
if ($airline->getCodeIata())
$sortedAirlines[] = $airline;
// Add NULL values to the end of the sorted array
foreach ($airlines as $airline)
if (!$airline->getCodeIata())
$sortedAirlines[] = $airline;
The downside to both of these is that you won't be able to do LIMITs in MySQL, so it might only work well for relatively small data sets.
Anyway, hope this gets you on your way!

Related

Use the Doctrine's QueryBuilder to get Categories with a minimum number of products

I want to use the Doctrine's QueryBuilder to get Categories with a specified attribute and a number minimum of products inside. I precise that's the first time I use this Doctrine's function. I discovered that and I realized that it's so much powerful than basics repositories functions. I'm a very beginner with SQL because I used to use Repository's functions.
But I think I achieve this in pure SQL:
SELECT category.*,COUNT(*)
FROM category_product
INNER JOIN category ON category_product.category_id = category.id
WHERE category.name = 'region'
GROUP BY category_product.category_id HAVING COUNT(*) > 20
At the moment I'm totally lost with DQL construction. My Category and Product Entities both have a Many to Many relationship and I can't reach traducing this to Doctrine. I tried using the category_product table auto-generated by doctrine's but It doesn't want to access this table...
$qb = $this->createQueryBuilder('c');
$qb
->select('c')
->where("c.name = 'region'")
->innerJoin('c.products', 'p', 'WITH', 'COUNT(c.products) > :minimum')
->setParameter('minimum', $minimum);
dump($qb->getQuery(), $qb->getQuery()->getResult());
Each Time I uses a new construction I have Semantical errors I'm not able to correct...
Thanks a lot for your help
You could try some thing like here (piece of code from my working model)
$qb = $this->createQueryBuilder('c')
->select('c')
->where('c.name = region')
->addSelect('COUNT(c.products) AS counter')
->innerJoin('c.products', 'p')
->groupby('p.id')
->having('count(p.id) >= :minimum')
->setParameter('region', $region)
->setParameter('minimum', $minimum)
;
dump($qb->getQuery()->getResult());
related post: here
Ok Thanks to you I got it.
I shouldn't select the COUNT because I typed the return to be an Array of categories but it returned an array of arrays with contains the category and the count... I had to group by c too.
Thanks a lot for your help !
The answer is :
$qb = $this->createQueryBuilder('c')
->select('c')
->where('c.name = :region')
->innerJoin('c.products', 'p')
->groupBy('c')
->having('SIZE(c.products) > :minimum')
->setParameter('region', $region)
->setParameter('minimum', 20);

Symfony doctrine using column value as array key when using joins

I've tried to find how to use column value as array key when getting results using queryBuilder. I found this question Using column value as array index in doctrine but unfortunately it doesn't work in my case as I'm using left join.
Currently I have this query:
$qb = $entityManager->createQueryBuilder('translation');
$qb->select('translation')
->from('MainBundle:PageTranslation', 'translation', 'translation.id')
->leftJoin('MainBundle:Page', 'page', 'WITH', 'IDENTITY(translation.page) = page.id')
->where('translation.locale = :locale')
->andWhere('translation.enabled = :enabled')
->andWhere('page.category = :category')
->setParameter('category', $category)
->setParameter('locale', $locale)
->setParameter('enabled', true);
$result = $qb->getQuery()->getResult();
Results I get has array keys starting with 0 and incrementing by one. I need array keys to be Page IDs (page.id) or IDENTITY of translation.page
If you look at the QueryBuilder API you'll see that the innerJoin() method has $indexBy as it's last argument. Pass 'translation.id' or ( the pk of translation instead of 'id') as the last argument, to get the desired result.

doctrine2: how to use the random custom function?

I use doctrine2 in the symfony2 framework. And I want to select the single random field.
I don't want to use the native query or to get the random with PHP.
I tried to make this according to
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/dql-user-defined-functions.html
https://gist.github.com/Ocramius/919465
The extended function connected in the config.yml
orm:
...
entity_managers:
default:
...
dql:
numeric_functions:
rand: MyProject\MyBundle\DQL\Rand
I try to call the query in the controller
$product= $em->createQueryBuilder('p')
->select('w')
->from('MyBundle:Product', 'p')
// ->orderBy('RAND ()') // I tried so
->addSelect('RAND() as HIDDEN rand') // and so
->orderBy('rand')
->getQuery()
->getSingleResult();
and I tried also
$product = $em->createQuery('SELECT p FROM MyBundle:Product p ORDER BY RAND()')
->setMaxResults(1)
->getSingleResult();
I get the error:
Error: Expected end of string, got '('
Note that, "ASC" follows after "RAND()" in the query.
I tried to call orderBy('RAND()', ''), but unsuccesfully...
With this bundle : https://github.com/mapado/MysqlDoctrineFunctions
You can use the functions in your DQL Query :
$query = 'SELECT RAND(), ROUND(123.45)
FROM ...
';
$em->createQuery($query);
But you can't use ->orderBy...
4.8.2. Can I sort by a function (for example ORDER BY RAND()) in DQL?
No, it is not supported to sort by function in DQL. If you need this functionality you should either use a native-query or come up with another solution. As a side note: Sorting with ORDER BY RAND() is painfully slow starting with 1000 rows.
http://docs.doctrine-project.org/en/2.1/reference/faq.html#can-i-sort-by-a-function-for-example-order-by-rand-in-dql

Propel multiple-parameter binding in where() clause

I'm trying to run this query on Propel 1.6 with symfony 1.4.20.
I want to bind 2 parameters onto this subquery but its not working.
$paginas = PaginaQuery::create()
->where("pagina.id not in (select id from cliente_artista where cliente_artista.cliente_id = ? and cliente_artista.culture = ?)"
,array('XXX', 'en')
)
->limit(5)
->find();
This gives me the error:
Cannot determine the column to bind to the parameter in clause
I also found this post but there is no answer (https://groups.google.com/forum/?fromgroups=#!topic/propel-users/2Ge8EsTgoBg)
Instead of using placeholders. You may use $id and $culture:
//first, get an array of the id's
//define your vars
$id = $your_id_param;
$culture = 'en';
$cliente_artistas = ClienteArtistaQuery::create()
->select('id')
->distinct()
->filterByClienteId($id)
->filterByCulture($culture)
->find();
$paginas = PaginaQuery::create()
->where("pagina.id NOT IN ?", $cliente_artistas)
->limit(5)
->find();
If this has to be done in one query, recommend using raw sql and binding the parameters into the PDO statement (but then you lose the convenience of PropelObjectCollections):
public function getResultSet($id, $culture) {
$id = $id_param;
$culture = $culture_param;
$sql = <<<ENDSQL
SELECT * from pagina
WHERE id NOT IN (SELECT distinct id
FROM cliente_artista
WHERE cliente_id = ?
AND culture = ?
)
LIMIT 5
ENDSQL;
$connection = Propel::getConnection();
$statement = $connection->prepare($sql);
$statement->bindValue(1, $id);
$statement->bindValue(2, $culture);
$statement->execute();
$resultset = $statement->fetchAll(PDO::FETCH_ASSOC); // or whatever you need
if (! count($resultset) >= 1) {
// Handle empty resultset
}
return $resultset;
}
You could also write some query methods to use propel orm query methods. Ofcourse, the propel api is beneficial reference. There are several ways to do this. I have indicated one method here which should work for you.
EDIT:
Here's an idea on doing this as one query [since useSelectQuery() requires 'relation' name], this idea assumes tables are not related but that id's are:
$paginas = PaginaQuery::create()
->addJoin(PaginaPeer::ID, ClienteArtistaPeer::CLIENTE_ID, Criteria::LEFT_JOIN)
->where("ClienteArtista.Id <> ?", $id)
->where("ClienteArtista.Culture <> ?", $culture)
->select(array('your','pagina','column','array'))
->limit(5)
->find();

symfony2 doctrine join

Okay, so i've got a query that i've researched and researched how to get this to work and for the life of me i cant!... perhaps i'm just doing this incorrectly and the minimal information ive found..
I've got a table named timeclock setup.. which has a field: noteBy_id in it which is an id to the user the record belongs to...
What I need to do now, is for the management side of things in the system.. I anticipate more than 1 company using this timeclock system, and as such I need to filter the results based on the company id.. In the user table, i have a field named parentcompany_id
So, lets see if I can express in words what I need to do..
I need to Select * from timeclock and left join user.parentcompany_id where timeclock.daydate < :start and where u.parentcompany = :pid
where start is: `date('Y-m-d 00:00:00');
I've setup this query:
$em = $this->getDoctrine()->getEntityManager();
$start = date('Y-m-d 00:00:00');
$qb = $em->getRepository('EcsCrmBundle:TimeClock');
$qb = $qb->createQueryBuilder('t');
$query = $qb->select('t, u.parentcompany_id')
->from('timeclock', 't')
->leftJoin('Ecs\AgentManagerBundle\Entity\User', 'u', 'ON' 'u.id = t.noteBy_id AND u.parentcompany_id = :pid')
->where('t.daydate < :start')
->andWhere("t.noteBy_id != ''")
->setParameter('start', $start)
->setParameter('pid', $user->getParentcompany())
->getQuery();
$entities = $query->getArrayResult();
I've looked and looked and can't find a solution to the error that I get which is:
An exception has been thrown during the rendering of a template ("[Semantical Error] line 0, col 112 near 'u ON u.id = t.noteBy_id': Error: Identification Variable Ecs\AgentManagerBundle\Entity\User used in join path expression but was not defined before.") in EcsCrmBundle:TimeClock:manager.html.twig at line 5.
and the query that gets output is:
SELECT t, u.parentcompany_id FROM Ecs\CrmBundle\Entity\TimeClock t LEFT JOIN Ecs\AgentManagerBundle\Entity\User u ON u.id = t.noteBy_id AND u.parentcompany_id = :pid, timeclock t LEFT JOIN Ecs\AgentManagerBundle\Entity\User u ON u.id = t.noteBy_id AND u.parentcompany_id = :pid WHERE t.daydate < :start AND t.noteBy_id != ''
which under normal circumstances would work perfectly... but in this, it just doesn't... Any ideas?
I've recently had to do it like this.. I'm guessing in this that your noteBy is a ManyToOne in the user table and you are wanting to have it filter the results by the company of the admin that is currently logged into your system..
So, adapting a join I had to write myself for such a task is easy enough. I personally like to use the QueryBuilder so this will be done in query builder..
Your first mistake in your query is the ->from('timeclock', 't') line. Because you have previously created your object with $qb = $em->getRepository('EcsCrmBundle:TimeClock'); $qb = $qb->createQueryBuilder('t'); you don't need the from in the query builder, as it will be generated for you.
The next issue, is the leftJoin and I'll explain why when I've shown you a working version.
And the last issue, preventing this from working how you want it - is a missing andWhere clause. So, lets take a look at a working query.
$query = $qb->select('t, u')
->leftJoin('t.noteBy', 'u', 'WITH', 'u.id = t.noteBy')
->where('t.daydate < :start')
->andWhere('u.parentcompany = :pid')
->setParameter('start', $start)
->setParameter('pid', $user->getParentcompany())
->getQuery();
So because we've already created the object by using $qb = $qb->createQueryBuilder('t') we just select t and u
For the join, we're joining the timeclock table by the noteBy column, which is the user id from the user table. So, the first argument being the "from" alias. So, since we've aliased the timeclock table with t we use t.noteBy. The next argument in the leftjoin is the alias of the 2nd table, which is u in this case but can be anything.. The third argument for a leftJoin anyway - is the way you join it.. either a WITH or ON will work here. and the 4th argument, is the match you wish it to have.. in this case u.id must equal t.noteBy
You will see that I got rid of one of the andWhere, I did this because with the properly structured query you shouldn't need it. I did however add in the andWhere for the u.parentcompany since that is afterall what you are looking to filter by you should have it in a WHERE instead of as a match in the join itself.
The documentation is very limited in this, and it took me a while to figure it all out as well.. You, undoubtedly - like me, came to using doctrine from writing your queries by hand. And So since you seem to be just starting with Symfony (i am myself as well about 2 months in now), you're still in the hand-coding mindset. But with further time, you'll start understanding the DQL way of life. Try this query out and see what happens.
Ok, first you would need to relate entity Timeclock to Company. Whenever you want to join two entities in Doctrine they need to be related by some attribute (that is, table column).
I don't see any need for User entity in this query as all info is available through Company entity and you are not filtering down results based on any user properties.
You desired query should look something like this (more or less). I took liberty and ditched _id suffixes from entity attributes as they tend to cloud what is really going on. ;)
$query = $this->getEntityManager()->createQuery("SELECT t, c.id FROM EcsCrmBundle:TimeClock t JOIN t.company c WHERE c.id = :pid AND t.daydate < :start AND t.noteBy != ''");
$query->setParameter('start', $start);
$query->setParameter('pid', $user->getParentcompany());
return $query->getArrayResult();
Also, I did inner-join (JOIN) as I think there could not be timeclock without it's company but feel free to change that to LEFT JOIN if that suits you better.
Is this what you were trying to achieve?

Resources