Skip to content
Advertisement

Excluding overlapped period in doctrine QueryBuilder

I have Product and Booking entity and I’m trying to select products that aren’t already booked in a given period. In other words, to select available products during a given period.

To make myself understandable, see the schema below. I only want to select product that have a booking like #1 or #5 since it is available. If a product has some booking like #2, #3, #4, #6, don’t select it since it is not available.

Each |--| represents a period, left side the startAt field and at the right side the endAt field.

Past                                              Futur 
|----------------------------------------------------->
                     Given period
                   |--------------|
     1             .              .
|---------|        .              .
                 2 .              .
             |-----+-|            .
                   .      3       .
                   .   |-----|    .
                   .              . 4
                   .            |-+-----|
                   .              .             5
                   .              .        |---------|
                   .       6      .
             |-----+--------------+-----|
                   .              .

My guess was to create a query like this:

class ProductRepository extends EntityRepository
{
    public function getAvailableProducts(DateTimeInterface $startAt, DateTimeInterface $endAt): array
    {
        return $this->createQueryBuilder('p')
            ->addSelect('b')
            ->join('p.bookings', 'b')
            ->andWhere('b.startAt > :endAt OR b.endAt < :startAt')
            ->setParameter('startAt', $startAt->format('Y-m-d H:i:s'))
            ->setParameter('endAt', $endAt->format('Y-m-d H:i:s'))
            ->getQuery()
            ->getResult()
        ;
    }
}

But when the andWhere() condition is met with #1 or #5 in the schema, the database (MYSQL 5.6) select this product even if #2, #3, #4 or #6 exist.

I am a little bit stuck with this query, and I feel I am going the wrong way so any help will be appreciated!

Advertisement

Answer

I found out that using a subquery with NOT EXISTS () instead of inner join is the best way to do:

SELECT *
FROM product p 
WHERE NOT EXISTS (
  SELECT * FROM booking b WHERE (
    (b.startAt between '2022-01-05 00:00:00' and '2022-01-15 00:00:00') OR 
    (b.endAt between '2022-01-05 00:00:00' and '2022-01-15 00:00:00') OR 
    ('2022-01-05 00:00:00' between b.startAt and b.endAt)
  ) AND b.product_id = p.id
) IN (1,2);

Here, the subquery find conflicting bookings (thanks @nice_dev in comments).

Doctrine repository:

class ProductRepository extends EntityRepository
{
    private EntityRepository $bookingRepository;

    public function __construct(BookingRepository $bookingRepository)
    {
        $this->bookingRepository = $bookingRepository;
    }

    public function getAvailableProducts(DateTimeInterface $startAt, DateTimeInterface $endAt): array
    {
        $bookingQueryBuilder = $this->bookingRepository->createQueryBuilder('b')
            ->andWhere('b.startAt between :startAt and :endAt OR b.endAt between :startAt and :endAt OR :startAt between b.startAt and b.endAt')
            ->andWhere('b.product = p')
        ;

        return $this->createQueryBuilder('p')
            ->andWhere(sprintf('not exists (%s)', $bookingQueryBuilder->getDQL()))
        ;
    }
}
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement