doctrine2: how to use the random custom function? - symfony

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

Related

Symfony3.4 / Doctrine : subquery in FROM Clause : Error: Class 'SELECT' is not defined

I'm working on a Symfony 3.4 project and I'm trying to translate an sql query to DQL query but I get an Issue.
Mysql Query:
select sum(montant_paye)
from
(select montant_paye
from vente
where client_id = 1
and montant_paye > 0
order by date ASC
limit 2)
as T;
DQL Query (Error):
return $this->getEntityManager()
->createQuery('
SELECT SUM(montantPaye) as Total
FROM
SELECT v.montantPaye
FROM AppBundle:Vente v
where v.montantPaye > 0
AND v.client = '.$clientId.'
ORDER BY v.date ASC
limit 2
')
->getResult();
Error :
[Semantical Error] line 0, col 71 near 'SELECT v.montantPaye
': Error: Class 'SELECT' is not defined.
Is any one have a solution for a correct DQL query ?
Quoting from Christophe stoef Coevoet (Symfony Core Developer):
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)
Details here.
add this function to your VenteRepository:
public function sumMontantPaye($clientId)
{
return $this->createQueryBuilder("v")
->select("sum(v.montantPaye) as sum")
->where("v.client = :id")
->andWhere("v.montantPaye > 0")
->setParameter("id", $clientId)
->setMaxResults(2)
->getQuery()->getSingleResult();
}
you can access the sum using $result["sum"] assuming $result is the variable assigned to this function in the controller

DQL Request with LeftJoin

I want to count how many times my theme is used in my Blog.
Any blog posts (articles in french) can use one or more theme.
So I have a table (ManyToMany) :
themes_articles (id_theme,id_article) And :
theme (id_theme,nom_theme)
articles (blog posts) (id_article, description ...)
In SQL, I do :
SELECT T.id,nom_theme,count(A.themes_id) from themes_articles A right join themes T on T.id=A.themes_id group by nom_theme
It works, but when I want to use right join in DQL, it's a little bit hard. I switched my two tables for use a left join but I don't know how I can use my relation table here (themes_articles).
I tried something like this :
$query = $this->createQueryBuilder('')
->select(array('T.id', 'nomTheme', 'count(A.themes_id) as nombre'))
->from('themes', 'T')
->leftJoin('themes_articles', 'A', 'WITH', 'T.id= A.themes_id')
->groupBy('nom_theme');
return $query->getQuery()->getResult();
But it doesn't work.
[Semantical Error] line 0, col 93 near 'themes_articles': Error: Class 'themes_articles' is not defined.
How can I do to convert my SQL request in DQL request ?
Thank you a lot for any help.
Use like this
$query = $this->createQueryBuilder()
->select(array('t.id', 't.nomTheme', 'count(ta.themes_id) as nombre'))
->from('<YOUR BUNDLE>:<Theam Entity Class>', 't') //Like AcmeTheamBundle:Themes
->leftJoin('t.themes_articles')
->groupBy('t.nomTheme'); //better to use theam id
return $query->getQuery()->getResult();
It is a little bit better ! I had to add the name of the relation between Articles and themes (contient) in my request.
$query = $this->createQueryBuilder('t')
->select(array('t.id', 't.nomTheme', 'count(ta.id) as nombre'))
->leftJoin('t.contient', 'ta', 'WITH', 't.id= ta.id')
->groupBy('t.nomTheme'); //better to use theam id
return $query->getQuery()->getResult();

Symfony and Doctrine: order by time difference

I am trying to build a query that retrieves all the most recent and upcoming activities from database.
The entity activity has a field named date of type DateTime. So in my repository I was thinking of building something like this:
$query = $repository
->createQueryBuilder('a');
$query->orderBy( 'DATEDIFF( a.date, NOW())' , 'ASC');
$query->setMaxResults( 6 );
return $query;
Unfortunately I get the following error:
[Syntax Error] line 0, col 59: Error: Expected end of string, got '('
The Dql that is generated by my query:
SELECT a FROM MyBundle\Entity\Activity a ORDER BY DATEDIFF( a.date, NOW()) ASC
I also tried installing beberlei/DoctrineExtensions, but either it is not working or I was unable to configure it correctly.
Anyone has any suggestion?
Thanks in advance
date_diff si already implemented as Doctrine DQL statement as described here
for use as ordering statement I suggest you to use the HIDDEN select keyword as explained in this article
So your DQL is like this:
SELECT
a,
DATE_DIFF( a.date, CURRENT_TIMESTAMP() ) AS HIDDEN score
FROM MyBundle:Entity a
ORDER BY score
And add the max result on the query. Let me know if you need help to adapt as query builder statement
Hope this help
Why don't you just use
$query = $repository
->createQueryBuilder('a');
$query->orderBy( 'DATEDIFF( a.date, CURRENT_TIMESTAMP())' , 'ASC');
$query->setMaxResults( 6 );
return $query;
?

How can I order by NULL in DQL?

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!

Doctrine Query Builder, "between" expression and subquery

I'm trying to create a quite complex query with Doctrine's Query Builder (I'm using Doctrine 2.2). In the model, i have a Distributor class and a DistributorVisit class with a one-to-many relationship. Every time a Distributor is visited by a representative, a new row is added to the DistributorVisit table with the visit date. An ER diagram of the two tables can be found here.
Now i want to be able to filter Distributors by their last visit date. So the user enters a date range (last visit from and last visit to) and Distributors whose last visit date is between those two dates are listed. I'm using Doctrine's Query Builder because there are a lot of other conditional queries I do in the filter, and I found the object-oriented approach to work best in this scenario. Here's what i did in the DistributorRepository class:
$qb = $this->getEntityManager()->createQueryBuilder()
->select('o')
->from('MyBundle:Distributor', 'o');
// Lots of 'andWhere's here
$qbv = $this->getEntityManager()->createQueryBuilder();
$qb->andWhere($qb->expr()->between(
$qbv->select($qbv->expr()->max('v.visitDate'))
->from('MyBundle:DistributorVisit', 'v')
->join('MyBundle:Distributor', 'o2',
Join::WITH,
$qbv->expr()->andX(
$qbv->expr()->eq('o2.id', 'v.distributorId'),
$qbv->expr()->eq('o2.id', 'o.id')
))
->getDQL(),
$filter->getLastVisitFrom()->getTimestamp(),
$filter->getLastVisitTo()->getTimestamp()
));
This gives me the following error:
[Syntax Error] line 0, col 83: Error: Expected Literal, got 'SELECT'
I guess this is because the Query Builder expects a literal where my sub-select is, however, the result of the sub-query should be a literal, right? May it be because the Query Builder does not add parenthesis accordingly?
Thanks a lot for your help.
I now resolved the issue the following way:
$qb = $this->getEntityManager()->createQueryBuilder()
->select('o')
->from('MyBundle:Distributor', 'o');
$qbdv = $this->getEntityManager()->createQueryBuilder();
$qbdv->select('MAX(dv2.visitDate)')
->from('MyBundle:DistributorVisit', 'dv2')
->where($qbdv->expr()->eq('dv2.distributor', 'o'));
$maxVisitDate = '('.$qbdv->getDQL().')';
$qb->leftJoin(
'o.distributorVisits',
'dv',
Join::WITH,
$qb->expr()->eq('dv.visitDate', $maxVisitDate)
);
$qb->andWhere(
$qb->expr()->between(
'dv.visitDate',
':dateFrom',
':dateTo'
)
)
->setParameter('dateFrom', $filter->getLastVisitFrom())
->setParameter('dateTo', $filter->getLastVisitTo());
So what I basically did is the following: I joined the DistributorVisit table to the Distributor table with the maximum visit date. The trick was the fact that one can pass the DQL of a (sub-)query ($qb1->getDQL()) directly to a Doctrine expression ($qb2->expr()->eq('column', $qb1->getDQL()). I did this with the left join in the code above.
I guess that your current DQL looks like this:
SELECT ..
FROM ..
WHERE SELECT .. FROM .. BETWEEN .. AND ..
But should look like this:
SELECT ..
FROM ..
WHERE (SELECT .. FROM ..) BETWEEN .. AND ..
To fix your code, i'd just put the subquery's dql inside parentheses:
$subQueryDQL = $qbv->select($qbv->expr()->max('v.visitDate'))
->from('MyBundle:DistributorVisit', 'v')
...
->getDQL();
$qb->andWhere($qb->expr()->between(
sprintf('(%s)', $subQueryDQL),
$filter->getLastVisitFrom()->getTimestamp(),
$filter->getLastVisitTo()->getTimestamp()
));
My case:
$qb->andWhere(
"t.field BETWEEN (
{$subQuerybuilder1->getDQL()}
) AND (
{$subQuerybuilder2->getDQL()}
)
");
Got:
SELECT ... WHERE t.field BETWEEN (
SELECT t1.field FROM t1
) AND (
SELECT t2.field FROM t2
)

Resources