Joining Across a Many-to-Many Relationship
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeEver wondered which starship in the fleet is crawling with the most droids? Me too! Let's list every ship in ascending order based on their droid count.
Dive into src/Controller/MainController.php. The query is: $ships = $repository->findIncomplete();.
Click into that method and give it a new, snazzy name: findIncompleteOrderedByDroidCount():
| // ... lines 1 - 14 | |
| class StarshipRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 17 - 24 | |
| public function findIncompleteOrderedByDroidCount(): Pagerfanta | |
| { | |
| // ... lines 27 - 34 | |
| } | |
| // ... lines 36 - 65 | |
| } |
Copy that, swing back to the controller, and replace the old method with the new one:
| // ... lines 1 - 10 | |
| class MainController extends AbstractController | |
| { | |
| ('/', name: 'app_homepage') | |
| public function homepage( | |
| // ... lines 15 - 16 | |
| ): Response { | |
| $ships = $repository->findIncompleteOrderedByDroidCount(); | |
| // ... lines 19 - 27 | |
| } | |
| } |
We've changed nothing yet, so a quick refresh gives us the same stuff.
To order the starships by their droid count, we need to join across the join table all the way to droid, group by starship, and then count the droids. Woof. Actually, it's quite nice!
In StarshipRepository, add a leftJoin(). But we're not going to think about the join table or the database. Nope, focus only on the relationships in Doctrine. So we're joining across s, which is our starship, and droids, the property that has the ManyToMany relationship to Droid. Finally, we alias those droids as droid.
To count the droids, add a groupBy('s.id').
To order replace the existing orderBy() with orderBy('COUNT(droid)', 'ASC'):
| // ... lines 1 - 14 | |
| class StarshipRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 17 - 24 | |
| public function findIncompleteOrderedByDroidCount(): Pagerfanta | |
| { | |
| $query = $this->createQueryBuilder('s') | |
| // ... line 28 | |
| ->orderBy('COUNT(droid)', 'ASC') | |
| ->leftJoin('s.droids', 'droid') | |
| ->groupBy('s.id') | |
| // ... lines 32 - 33 | |
| ; | |
| // ... lines 35 - 36 | |
| } | |
| // ... lines 38 - 67 | |
| } |
After that, hit refresh and boom! At the top, you'll see droids none. But as you scroll down, the droid count increases. If you're brave enough to venture a few pages ahead, we start to see starships with two, three, or even four droids!
The key? There's nothing special about this join. We join across the property and Doctrine handles the rest.
If you peek at the query on this page, you'll see it's handling all the details. Search for starship_droid to find the query. This is ugly, but if you format the query, it selects from starship, taking care of the join over to the join table and joining again over to droid. That allows us to count and order by that count on that droid table. Impressive Doctrine, impressive!
That's technically it for ManyToMany! But next we're going to handle a more advanced, but still common, use case: adding data to the join table, like the date when the droid joined the starship.
11 Comments
Hi,
I also only got a single row with the groupBy, I had to add COUNT(droid) AS HIDDEN droidCount in the select.
Hey @Cedric
I just gave it a try, and the query works well. I'm confused why adding a select changes the number of fetched rows. Did you download the course code from this page or are you coding it from scratch?
Cheers!
Hi @MolloKhan,
Thanks for your quick reply. I just tried again by mounting the Docker container with the Postgres database, and everything works.
I was using a local MariaDB database during the course.
Ohh that makes sense. I'm glad you found a solution. Cheers!
Hello! Loving this course. When I use a mysql database I get this error after updating the findIncompleteOrderedByDroidCount() function
An exception has been thrown during the rendering of a template ("An exception occurred while executing a query: SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #10 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'starshop2.s1_.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by").Tried a few things to fix it but I'm coming up blank. Thanks!Hey @Logan-M
We're glad to hear you're liking our content. Can I see your query/code?
Cheers!
Here is my code below. The only difference I notice is the use statement for starshipStatusEnum (Mine is in a different location). I am using a MySQL database not the PostgreSQL.
`<?php
namespace App\Repository;
use App\Entity\Starship;
use App\Model\StarshipStatusEnum;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Pagerfanta;
/**
*/
class StarshipRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
}
/**
*/
public function findIncompleteOrderedByDroidCount(): Pagerfanta
{
$query = $this->createQueryBuilder('s')
;
return new Pagerfanta(new QueryAdapter($query));
}
public function findMyShip(): Starship
{
return $this->findAll()[0];
}
// /**
// @return Starship[] Returns an array of Starship objects // /
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('s.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Starship
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
`
The raw query looks like this:
SELECT s0_.id AS id_0, s0_.name AS name_1, s0_.captain AS captain_2, s0_.class AS class_3, s0_.status AS status_4, s0_.arrived_at AS arrived_at_5, s0_.slug AS slug_6, s0_.created_at AS created_at_7, s0_.updated_at AS updated_at_8 FROM starship s0_ LEFT JOIN starship_droid s2_ ON s0_.id = s2_.starship_id LEFT JOIN droid d1_ ON d1_.id = s2_.droid_id WHERE s0_.status <> ? GROUP BY s0_.id ORDER BY COUNT(d1_.id) ASCThanks for taking the time to look at this.
Hey @Logan-M!
Ah, in recent versions of MySQL, GROUP BY is more strict than Postgres. Basically, you can't do dynamic operations like we're doing in
->orderBy()- it needs to be a column.Take a look at this comment above for, I think, the solution - this should work in MySQL and Postgres.
I hope this helps!
Kevin
Thanks! That makes sense.
In the last step, PagerFanta doesn't work. I only see one ship and 14 pages.
Hey @giorgiocba
I just gave this a try (with the new code) and it works fine. Could you copy-paste the AppFixtures code from this code block and try again?
https://symfonycasts.com/screencast/doctrine-relations/many-to-many-foundry#codeblock-3d67ea2ef7
Cheers!
"Houston: no signs of life"
Start the conversation!