Im trying to get the best rated movies by average from my database and hydrate them nicely into a DTO with doctrine so i can work well later with it and integrate them e.g. into my api with api platform.
I already managed to get it work with a raw SQL query but i cannot manage to get it work with hydration into a DTO with doctrine.
namespace App\Repository;
use App\Entity\Movie;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class MovieRepository extends ServiceEntityRepository
{
public function findBestRated()
{
$sql = "
SELECT m.*, AVG(Cast(r.rating as Decimal)) AS avg_rating, COUNT(r.id) AS count_rating
FROM `movie` m
JOIN rating r ON r.movie_id = m.id
GROUP BY m.id
ORDER BY avg_rating DESC
LIMIT 10
";
$connection = $this->getEntityManager()->getConnection();
$statement = $connection->prepare($sql);
$result = $statement->executeQuery();
return $result->fetchAllAssociative();
}
}
I would like to use a DTO like below:
namespace App\Dto;
use App\Entity\Movie;
class RatedMovie
{
public Movie $movie;
public float $averageRating;
public int $ratingCount;
public function __construct(Movie $movie, float $averageRating, int $ratingCount)
{
$this->movie = $movie;
$this->averageRating = $averageRating;
$this->ratingCount = $ratingCount;
}
}
I found some information on https://geek-week.imtqy.com/articles/en496166/index.html, but still i cannot get the hydration running. I already tried with ResultSetMapping and ResultSetMappingBuilder and a native doctrine query. With ResultSetMappingBuilder i can kind of simulate my raw sql query but the result is still an associative array and not mapped into the RatedMovie DTO.
public function findBestRated()
{
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
$rsm->addScalarResult('id', 'movieId');
$rsm->addScalarResult('name', 'movieName');
$rsm->addScalarResult('avg_rating', 'averageRating', Types::FLOAT);
$rsm->addScalarResult('count_rating', 'countRating', Types::INTEGER);
$sql = "
SELECT m.*, AVG(r.rating) AS avg_rating, COUNT(r.id) AS count_rating
FROM `movie` m
JOIN rating r ON r.name_id = m.id
GROUP BY m.id
ORDER BY avg_rating DESC
LIMIT 10
";
$query = $this->getEntityManager()->createNativeQuery($sql, $rsm);
return $query->getResult();
}
I cannot get it running with newObjectMappings or the kind of strange syntax SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM ... from the linked article. Any ideas?
Each Disque (d) has an associated collection of Note (n).
My querybuilder is to get every Disque based on how many Note are associated.
Here's a peak on the classes
class Disque
{
...
/**
* #ORM\ManyToMany(targetEntity="Note", cascade={"persist"})
*/
private $notes;
...
}
class Note
{
...
/**
* #ORM\ManyToOne(targetEntity="NoteValeur", cascade={"persist"})
* #ORM\JoinColumn(nullable=true)
*/
private $noteValeur;
...
}
class NoteValeur
{
...
/**
* #Gedmo\Slug(fields={"titre"})
* #ORM\Column(name="slug", unique=true, length=32)
*/
private $slug;
}
I'm using Doctrine Querybuilder to fetch the results, using a count for setting the threshold.
<?php
...
class DisqueRepository extends \Doctrine\ORM\EntityRepository
{
public function getDisquesByNotesAndAllInfo($slug, $seuil, $page, $nbPerPage)
{
$q = $this->createQueryBuilder('d');
$q
->select('d as infosDisque')
->leftJoin('d.pochettes', 'p')
->addSelect('p')
->leftJoin('d.groupes','g')
->addSelect('g')
->leftJoin('d.labelDisque', 'l')
->addSelect('l')
->leftJoin('d.chroniques','c')
->addSelect('c')
->leftJoin('d.notes', 'n')
->addSelect('n')
->innerJoin('n.noteValeur','nv')
->addSelect('nv')
->groupBy('d')
->addSelect('COUNT(d) as NbNotes')
->where('nv.slug LIKE :slug')
->setParameter('slug', $slug)
->having($q->expr()->gte('NbNotes',':seuil'))
->setParameter('seuil', $seuil)
->setFirstResult( ($page-1) * $nbPerPage)
->setMaxResults($nbPerPage)
->orderBy('d.dateSortieDisque', 'desc')
;
return new Paginator($q, true);
}
}
The result set is perfectly fine. The problem is that when I change the $page value, the offset in the SQL is not increased. I'm getting the same range of results no matter what.
The Symfony profiler gives me the following executable query
SELECT DISTINCT id_44
FROM (
SELECT
COUNT(s0_.id) AS sclr_0, s0_.id AS id_1, s0_.titre AS titre_2, s0_.datesortiedisque AS datesortiedisque_3, s0_.nombreDisque AS nombreDisque_4, s0_.remarque AS remarque_5, s0_.tracklist AS tracklist_6, s0_.lineup AS lineup_7, s0_.slug AS slug_8, s0_.coderef AS coderef_9,
s1_.id AS id_10, s1_.alt AS alt_11, s1_.url AS url_12, s1_.coderef AS coderef_13, s1_.ordre AS ordre_14, s1_.fichier AS fichier_15,
s2_.id AS id_16, s2_.nom AS nom_17, s2_.site AS site_18, s2_.motto AS motto_19, s2_.popularite AS popularite_20, s2_.slug AS slug_21, s2_.coderef AS coderef_22,
s3_.id AS id_23, s3_.titre AS titre_24, s3_.slug AS slug_25, s3_.coderef AS coderef_26,
s4_.id AS id_27, s4_.texte AS texte_28, s4_.resume AS resume_29, s4_.dateSaisie AS dateSaisie_30, s4_.lectures AS lectures_31, s4_.slug AS slug_32, s4_.coderef AS coderef_33,
s5_.id AS id_34, s5_.dateSaisie AS dateSaisie_35, s5_.coderef AS coderef_36, s6_.id AS id_37,
s6_.titre AS titre_38, s6_.valeur AS valeur_39, s6_.image AS image_40, s6_.coderef AS coderef_41, s6_.octal AS octal_42, s6_.slug AS slug_43,
s0_.id AS id_44, s0_.titre AS titre_45, s0_.datesortiedisque AS datesortiedisque_46, s0_.nombreDisque AS nombreDisque_47, s0_.remarque AS remarque_48, s0_.tracklist AS tracklist_49, s0_.lineup AS lineup_50, s0_.slug AS slug_51, s0_.coderef AS coderef_52
FROM
se_disque s0_
LEFT JOIN disque_pochette d7_ ON s0_.id = d7_.disque_id
LEFT JOIN se_pochette s1_ ON s1_.id = d7_.pochette_id
LEFT JOIN disque_groupe d8_ ON s0_.id = d8_.disque_id
LEFT JOIN se_groupe s2_ ON s2_.id = d8_.groupe_id
LEFT JOIN se_labeldisque s3_ ON s0_.label_disque_id = s3_.id
LEFT JOIN disque_chronique d9_ ON s0_.id = d9_.disque_id
LEFT JOIN se_chronique s4_ ON s4_.id = d9_.chronique_id
LEFT JOIN disque_note d10_ ON s0_.id = d10_.disque_id
LEFT JOIN se_note s5_ ON s5_.id = d10_.note_id
INNER JOIN se_notevaleur s6_ ON s5_.note_valeur_id = s6_.id
WHERE
s6_.slug LIKE 'classique'
GROUP BY
s0_.id, s0_.titre, s0_.datesortiedisque, s0_.nombreDisque, s0_.remarque, s0_.tracklist, s0_.lineup, s0_.slug, s0_.coderef, s0_.label_disque_id, s0_.format_id, s0_.format_discographique_id
HAVING
sclr_0 >= 2
) dctrn_result
ORDER BY datesortiedisque_3 DESC LIMIT 10 OFFSET 10
I can't seem to change the offset in this query no matter what I pass to the $q->setFirstResult() method.
Does anybody have a clue ?
Well, guess what, I was using a debug value in my controller and I had forgotten about it :
$disques = $dr->getDisquesByNotesAndAllInfo($slug, $seuil, 2, $nbPerPage);
All works fine now that I changed back this messy "2" value.
Sorry about that. Thanks to both of you for your help !
I try to use "distinct on" with doctrine but I get the following error:
Error: Expected known function, got 'on'
class Report extends EntityRepository
{
public function findForList() {
$queryBuilder = $this->createQueryBuilder('r');
$queryBuilder->select('distinct on (r.parentId)')
->orderBy('r.parentId')
->orderBy('r.date', 'DESC');
return $queryBuilder->getQuery()->getResult();
}
}
How could I implement the following query?
select distinct on (r.parent_id)
r.parent_id,
r.id,
r.name
from frontend.report r
order by r.parent_id, r.date desc;
Apparently it doesn't seem possible to do this with the query builder. I tried to rewrite my query in different ways:
select * from frontend.report r
where
r.id in (
select distinct
(select r3.id from frontend.report r3
where r3.parent_id = r.parent_id
order by r3.date desc limit 1) AS id
from frontend.report r2);
But doctrine doesn't support LIMIT:
public function findForList() {
$qb3 = $this->_em->createQueryBuilder();
$qb3->select('r3.id')
->from('MyBundle:frontend\report', 'r3')
->where('r3.parentId = r2.parentId')
->orderBy('r3.date', 'DESC')
//->setMaxResults(1)
;
$qb2 = $this->_em->createQueryBuilder();
$qb2->select('(' . $qb3->getDql() . ' LIMIT 1)')
->from('MyBundle:frontend\report', 'r2')
->distinct(); // groupBy('r2.parentId')
$queryBuilder = $this->createQueryBuilder('r');
$queryBuilder->where(
$queryBuilder->expr()->in('rlt.id', $qb2->getDql())
);
return $queryBuilder->getQuery()->getResult();
}
I think the only solution is to use native SQL queries.
This response is for someone that yet looking for the solution in similar cases.
If you need a sample "DISTINCT" you can use:
$queryBuinder->select('parentId')
->distinct('parentId');
but if you want a "DISTINCT ON" you should use Native SQL instead of Query Builder, check out the manual here: Doctrine Native SQL
Just remove the on word :
$queryBuilder->select('distinct r.parentId')
->orderBy('r.parentId')
->orderBy('r.date', 'DESC');
I have been trying to pull out a random row, I have used this:
This is the example code I found which did not really help ( I found here: https://gist.github.com/pierroweb/1518601)
class QuestionRepository extends EntityRepository
{
public function findOneRandom()
{
$em = $this->getEntityManager();
$max = $em->createQuery('
SELECT MAX(q.id) FROM EnzimQuestionBundle:Question q
')
->getSingleScalarResult();
return $em->createQuery('
SELECT q FROM EnzimQuestionBundle:Question q
WHERE q.id >= :rand
ORDER BY q.id ASC
')
->setParameter('rand',rand(0,$max))
->setMaxResults(1)
->getSingleResult();
}
}
Now I have something like this:
$em = $this->getEntityManager();
$max = $em->createQuery('SELECT MAX(p.id) FROM GreenMonkeyDevGlassShopBundle:Product p')->getSingleScalarResult();
return $em->createQuery('SELECT p FROM GreenMonkeyDevGlassShopBundle:Product p INNER JOIN (SELECT p2.categories. FROM GreenMonkeyDevGlassShopBundle:Product p.categories WHERE :cid IN(pc) p.id >= :rand ORDER BY p.id ASC')
->setParameter('cid', $category_id)
->setParameter('rand',rand(0,$max))
->setMaxResults(intval($limit))
->getSingleResult();
I keep getting this error though:
FatalErrorException: Error: Call to undefined method Doctrine\ORM\Query\ResultSetMapping::addRootEntityFromClassMetadata() in /var/www/gmd-milkywayglass/src/GreenMonkeyDev/GlassShopBundle/Entity/CategoryRepository.php line 42
Any thoughts on what I can be doing wrong? I know that Doctrine does not have a get random method. Maybe there is some simple solution? Thank you!
You should try to write your own DQL function in order to use RAND() in your query :
namespace My\Custom\Doctrine2\Function;
/**
* RandFunction ::= "RAND" "(" ")"
*/
class Rand extends FunctionNode
{
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'RAND()';
}
}
After you have to register this DQL function in symfony2 config.yml
doctrine:
dbal:
# ...
orm:
#...
dql:
numeric_functions:
RAND: My\Custom\Doctrine2\Function
For more informations, read the following links
DQL User Defined Functions
How to Register Custom DQL Functions in Symfony2
I've got an SQL query that returns all the rows in one table (country) which have a related entry in another table (ducks) but I'm struggling to turn this into DQL. This is a standard one-many relationship as each country can have multiple ducks, I believe it is all set up correctly as I can return ducks within a country and return the country a duck is in using standard code.
The working query is:
SELECT c.* FROM country c
INNER JOIN ducks d
ON c.id = d.country_id
GROUP BY c.country
ORDER BY c.country ASC
I've tried converting this to:
SELECT c FROM WfukDuckBundle:Country c
INNER JOIN WfukDuckBundle:Ducks d
ON c.id = d.country_id
GROUP BY c.country
ORDER BY c.country ASC
which produces the following error:
[Semantical Error] line 0, col 79 near 'd ON': Error: Identification Variable
WfukDuckBundle:Ducks used in join path expression but was not defined before.
I'm quite new to Symfony/Doctrine so I suspect it's probably something obvious!
I'm using Symfony 2.0.11 with doctrine
Update:
I've Also tried:
SELECT c FROM WfukDuckBundle:Country c
INNER JOIN c.ducks d
ON c.id = d.country_id
GROUP BY c.country
ORDER BY c.country ASC
where 'ducks' is defined in the Country class as:
/**
* #ORM\OneToMany(targetEntity="Ducks", mappedBy="country")
*/
protected $ducks;
public function __construct()
{
$this->ducks = new ArrayCollection();
}
the definition for country in the ducks class is:
/**
* #ORM\ManyToOne(targetEntity="Country", inversedBy="ducks")
* #ORM\JoinColumn(name="country_id", referencedColumnName="id")
*/
private $country;
Do yourself a favour and use the query builder. Easier to read and update and reuse your queries
<?php
namespace Vendor\Prefix\Repository;
use Doctrine\ORM\EntityRepository;
class SomeRepository extends EntityRepository
{
public function countryDucks()
{
// $em is the entity manager
$qb = $em->createQueryBuilder();
$qb
->select('country', 'duck')
->from('WfukDuckBundle:Country', 'country')
->innerJoin('country.ducks', 'duck')
->groupBy('country.country')
->orderBy('country.country', 'ASC')
;
$query = $qb->getQuery();
// Potential Hydration Modes
// --------------------------------
// Doctrine\ORM\Query::HYDRATE_OBJECT
// Will give you an array of your object entities
// --------------------------------
// Doctrine\ORM\Query::HYDRATE_ARRAY
// Will give you an array mimicking
// your object graph
// --------------------------------
return $query->getResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
}
}