DQL many to many and count
Asked Answered
A

3

7

I'm using Symfony 2 with Doctrine, and I've got two entities joined in a many to many association. Let's say I have two entities: User and Group, and the related tables on db are users, groups and users_groups.

I'd like to get the top 10 most populated groups in DQL, but I don't know the syntax to perform queries on the join table (users_groups). I already looked on the Doctrine manual but I didn't found the solution, I guess I still have a lot to learn about DQL.

In plain sql that would be:

select distinct group_id, count(*) as cnt from users_groups group by group_id order by cnt desc limit 10

Can you please help me to translate this to DQL?

Update (classes):

/**
 * Entity\E_User
 *
 * @ORM\Table(name="users")
 * @ORM\Entity
 */
class E_User
{
    /**
     * @ORM\ManyToMany(targetEntity="E_Group", cascade={"persist"})
     * @ORM\JoinTable(name="users_groups",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="cascade")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id", onDelete="cascade")}
     * )
     */

    protected $groups;

    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string $name
     *
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;

    /* ... other attributes & getters and setters ...*/
}


/**
 * Entity\E_Group
 *
 * @ORM\Table(name="groups")
 * @ORM\Entity
 */
class E_Group
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string $name
     *
     * @ORM\Column(name="text", type="string", length=255)
     */
    private $name;

    /* ... other attributes & getters and setters ...*/
}
Avenge answered 16/4, 2012 at 14:9 Comment(0)
I
7

It's not easy without seeing the actual classes, but by guessing you have a many-to-many bidirectional relationship:

$dql = "SELECT g.id, count(u.id) as cnt FROM Entity\Group g " .
    "JOIN g.users u GROUP BY g.id ORDER BY cnt DESC LIMIT 10;";
$query = $em->createQuery($dql);
$popularGroups = $query->getArrayResult();

UPDATE:

You don't have to use a bidirectional relationship, you can query the other way around:

$dql = "SELECT g.id, count(u.id) as cnt FROM Entity\User u " .
    "JOIN u.groups g GROUP BY g.id ORDER BY cnt DESC LIMIT 10;";
Improvvisatore answered 19/4, 2012 at 2:9 Comment(2)
thanks, the problem is that Group entity doesn't have an association called users. So I get the exception: [Semantical Error] line 0, col 72 near 's GROUP BY g.id': Error: Class Entity\E_Group has no association named users. I updated the question with the actual classes. Shall I add a $users attribute to E_Group entity to make the bidirectional many-to-many relationship?Avenge
many thanks. I ended up with a bidirectional relationship, anyway. That was just a dummy query to understand how it works. thanks againAvenge
F
1

For those who want to build the query with Doctrine's QueryBuilder instead of using DQL directly take this solution.

Please note that my problem wasn't to get the top user groups, but technically the problem is pretty similar to mine. I work with posts (like articles/blog posts) and tags that are added to posts. I needed to determine a list of related posts (identified by same tags). That list had to be sorted by relevance (the more same tags another post has the more relevant it is).

This is the method of my PostRepository class:

/**
 * Finds all related posts sorted by relavance
 * (from most important to least important) using
 * the tags of the given post entity.
 *
 * @param Post $post
 *
 * @return POST[]
 */
public function findRelatedPosts(Post $post) {
    //  build query
    $q = $this->createQueryBuilder('p')
        ->addSelect('count(t.id) as relevance')
        ->innerJoin('p.tags', 't')
        ->where('t.id IN (:tags)')
        ->setParameter('tags', $post->getTags())
        ->andWhere('p.id != :post')
        ->setParameter('post', $post->getId())
        ->addGroupBy('p.id')
        ->addOrderBy('relevance', 'DESC')
        ->getQuery();

    //  execute query and retrieve database result
    $r = $q->execute();

    //  result contains arrays, each array contains
    //  the actual post and the relevance value
    //  --> let's extract the post entities and
    //  forget about the relevance, because the
    //  posts are already sorted by relevance
    $r = array_map(function ($entry) {
        //  first index is the post, second one
        //  is the relevance, just return the post
        return reset($entry);
    }, $r);

    //  array of posts
    return $r;
}

Thank you @Tom Imrei for you solution. Also the answer #26549597 was very helpful.

Frequency answered 17/11, 2016 at 11:32 Comment(0)
T
0

To improve Tom's answer, you could use DQL's HIDDEN keyword. This way, the result doesn't contain the useless cnt column and Arvid's array_map solution isn't needed (which could speed up the result significantly for larger queries). And the OP's question was to get the top 10 groups, not just their IDs.

It would look something like this:

$query = 'SELECT g, HIDDEN COUNT(u.id) AS cnt FROM Entity\Group g LEFT JOIN g.users u ORDER BY cnt DESC';
$groups = $em->createQuery($query)->setMaxResults(10)->getResult();

Also, note the use of LEFT JOIN that ensures that empty groups are not dismissed.

Tees answered 29/6, 2020 at 7:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.