排除学说QueryBuilder中的重叠时期

Excluding overlapped period in doctrine QueryBuilder

我有 ProductBooking 实体,我正在尝试 select 在给定时间段内尚未预订的产品。换句话说,在给定期间 select 可用产品。

为了便于理解,请参阅下面的架构。我只想要 select 有像 #1 或 #5 这样的预订的产品,因为它是可用的。如果某个产品有一些预订,例如 #2、#3、#4、#6,请不要 select 因为它不可用。

每个 |--| 代表一个句点,左边是 startAt 字段,右边是 endAt 字段。

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

我的猜测是创建这样的查询:

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()
        ;
    }
}

但是当andWhere()条件满足schema中的#1或#5时,数据库(MYSQL5.6)select这个产品就算#2,#3 , #4 或 #6 存在。

我对这个查询有点困惑,我觉得我走错了路,所以我们将不胜感激!

产品may/may在给定时间段内没有预订。要检测重叠或碰撞,您最多需要检查 3 个条件:

  • 当前行的开始日期在所选日期期间
  • 当前行结束日期介于所选日期期间
  • 所选时间段的开始日期介于当前行的开始日期和结束日期之间。

原始 SQL 查询:

SELECT count(*) as 'booked_count'
FROM product p 
INNER JOIN booking b ON p.id = b.product_id 
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 p.id IN (1,2);

Fiddle Demo

Doctrine QueryBuilder:

<?php

class ProductRepository extends EntityRepository
{
    public function getAvailableProducts(\DateTimeInterface $startAt, \DateTimeInterface $endAt): array
    {
        return $this->createQueryBuilder('p')
            ->addSelect('count(b.*) as cnt')
            ->join('p.bookings', 'b')
            ->where('b.startAt between :startAt and :endAt')
            ->orWhere('b.endAt between :startAt and :endAt')
            ->orWhere(':startAt between b.startAt and b.endAt')
            ->setParameter('startAt', $startAt->format('Y-m-d H:i:s'))
            ->setParameter('endAt', $endAt->format('Y-m-d H:i:s'))
            ->getQuery()
            ->getResult()
        ;
    }
}

注意: 这样您可以检查检索到的记录的 计数 并相应地继续。

我发现使用带有 NOT EXISTS () 的子查询而不是内部连接是最好的方法:

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);

在这里,子查询找到了冲突的预订(感谢 中的@nice_dev)。

学说存储库:

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()))
        ;
    }
}