11

As the title states,

I would like to run 1 query to get results from a table with the count of their respective relationships.

Lets say I have a Person entity with a OneToMany relationship with a Friend entity

The Person entity might look something like the following:

class Person
{
    /**
     * @ORM\OneToMany(...)
     */
    protected $friends;

    public function __construct()
    {
        $this->friends = new ArrayCollection();
    }

    ...
}

A classic SQL solution to what I want to achieve might look like the following:

SELECT p.*, COUNT(f.id) as friendsCount
FROM Persons p
LEFT JOIN Friends f
ON f.person_id = p.id
GROUP BY p.id

Now I am wondering if this could be done in DQL as well and storing the count value into the Person Entity

Lets say I expand the Person entity like: (Keep in mind this is just an idea)

class Person
{
    /**
     * @ORM\OneToMany(...)
     */
    protected $friends;

    protected $friendsCount;

    public method __construct()
    {
        $this->friends = new ArrayCollection();
    }

    ...

    public function getFriendsCount()
    {
        return $this->friendsCount;
    }
}

Now I am having trouble finding how I could populate the count value in the entity from DQL:

SELECT p, /* What comes here ? */
FROM AppBundle\Entity\Person p
LEFT JOIN p.friends f
GROUP BY p.id

PS: I do know I could just do:

$person->getFriends()->count();

And even mark it as extra lazy to get the count result.

I just though this count relationships example demonstrated well what I am trying to do.

(Which is populating the entity's non @ORM\Column properties from dql)

Is this possible with Doctrine ?

Is this breaking some solid principles ? (SRP ?)

Cookie for your thoughs ;)

RVandersteen
  • 2,067
  • 2
  • 25
  • 46

1 Answers1

12

You probably just want to select the count as you need it, as described above with $person->getFriends()->count();. However, you can select both an object and a count at the same time (see these Doctrine query examples), there is one very similar to what you are doing:

SELECT p, COUNT(p.friends)
FROM AppBundle\Entity\Person p
LEFT JOIN p.friends f
GROUP BY p.id

What should be returned back is an array of arrays with the object and count, like so:

[
    [Person, int],
    [Person, int],
    ...,
]

Your best bet would be to make that a Repository call on your PersonRepository, something like findPersonsWithFriendCount(), which would look like:

public function findPersonsWithFriendCount()
{
    $persons = array();

    $query = $this->_em->createQuery('
        SELECT p, COUNT(p.friends)
        FROM AppBundle\Entity\Person p
        LEFT JOIN p.friends f
        GROUP BY p.id
    ');

    $results = $query->getResult();

    foreach ($results as $result) {
        $person = $result[0];
        $person->setFriendsCount($result[1]);

        $persons[] = $person;
    }

    return $persons;
}

Keep in mind you'd need to add a setFriendsCount() function on your Person object. You could also write a native SQL query and use result set mapping to automatically map your raw result's columns to your entity fields.

Jason Roman
  • 8,146
  • 10
  • 35
  • 40
  • Thanks a lot for this example ! Really helpfull – RVandersteen Feb 02 '16 at 18:59
  • 1
    Keep in mind that this shouldn't be necessary. If you simply explicitly join in a repository call like this, you can just remove the `COUNT()` and return entities as usual, then put `return $this->friends->count()` in your `getFriendsCount()` method. If you explicitly join it won't add another database call...and if you haven't loaded them yet it will. – Jason Roman Feb 02 '16 at 21:11
  • Good point indeed, I might even not explicitly need the getFriendsCount() and use getFriends()->count() where I need it after the join. But this technique might come out usefull for some special queries. What would be your thoughs on (very) large numbers of relations and performance. Joining (and thus hydrating) the large amount of entities just to get the count might slow things down. Just thinking (writing) out loud :) – RVandersteen Feb 02 '16 at 21:54
  • My usual way of working is do it using the ORM and simplest way first...then if there are performance hits, start looking at things like raw queries, removing the hydration, caching, etc. I try not to pre-optimize too much unless it's something like using joins to prevent a page from running 50 additional queries or something like that. – Jason Roman Feb 02 '16 at 22:06
  • It is indeed an effective way of doing things. Thank you for sharing your thoughs – RVandersteen Feb 02 '16 at 22:12
  • 1
    I had to update original query to this `SELECT p, COUNT(f) ...` to make it work. – Dimitry K Apr 06 '17 at 15:36