Search code examples
inner-joinsymfony4doctrine-query

Inner join on many to many relation


I have those 2 entities in my symfony project : house and software.

Many Home can have many software and Many software can belongs to many home

I'm trying to get only the Homes that have , let's say the software n° 1 + software n°2.

Actually I've managed to retrieve the Homes that have software n°1 and those that have software n°2 but not both those that have soft 1 + soft2

If I'm not wrong, It should be a Inner join join, right ?

Here's my entities and repository's method :

    class Software {
          /**
           * @ORM\ManyToMany(targetEntity="App\Entity\Home", mappedBy="softwares")
           */
           private $homes;

           public function __constuct() {
               $this->homes = new ArrayCollection();
           }

           // ...

           public function getHomes(){ ... }
           public function addHome(Home $home){ ... }
           // ...

    }


    class Home {
          /**
           * @ORM\ManyToMany(targetEntity="App\Entity\Software", inversedBy="homes")
           */
           private $softwares;

           public function __constuct() {
               $this->softwares = new ArrayCollection();
           }

           //...

           public function getSoftwares(){ ... }
           public function addSoftware(Software $software){ ... }
           //...

    }

Home repository

    class HomeRepository extends ServiceEntityRepository {
          public function innerJoinSoftware($softIds)
          {
               $qb = $this->createQueryBuilder('c')
                   ->innerJoin('c.softwares', 's')
                   ->andWhere('s.id IN(:softIds)')
                     ->setParameter('softIds', $softIds)
              ;
              return $qb->getQuery()->getResult();
         }
    }

To illustrate my point :

  • Home1 has soft1, soft2

  • Home2 has soft1, soft3

  • Home3 has soft2, soft3

What I wanna do is something like

  dump(homeRepo->innerJoinSoftware([1, 2]));
  //should output Home1 but actually I have
  //it outputs Home1, Home2, Home3

Here's the SQL version I came out with, but I'm still not able to do it with Doctrine

  SELECT home.id, home.name FROM Home as home
  INNER JOIN (
        SELECT home_id as home_id, COUNT(home_id) as count_home
        FROM home_software
        WHERE software_id IN (1, 2)
        GROUP BY home_id
        HAVING count_home = 2) as soft # count_home should be dynamic 
  ON home.id = soft.home_id
  ORDER BY home.name

Solution

  • Here's how I solve this problem (helped by the raw SQL I've posted)

    public function findBySoftwaresIn($softIds)
    {
        //retrieve nbr of soft per home
        $nbrSoftToFind = count($softIds);
        $qb = $this->createQueryBuilder('h');
    
        $qb->innerJoin('h.softwares', 's')
            ->andWhere('h.id IN (:softIds)')
                ->setParameter('softIds', $softIds)
    
            //looking for home coming back by nbrSoft
            ->andHaving($qb->expr()->eq($qb->expr()->count('h.id'), $nbrSoftToFind))
            ->groupBy('h.id')//dont forget to group by ID
            ->addOrderBy('h.name')
        ;
    
        return $qb->getQuery()->getResult();
    }