使用Django Q查询筛选首次预订客户ID失败求助
问题:Django Q查询筛选指定年月首次完成预订客户ID失败
我尝试用Django的Q查询筛选指定年月内首次完成预订的客户ID,编写了如下函数:
def get_first_time_customer_ids(year: int, month: int) -> QuerySet: return Customer.objects.filter( Q(bookings__status=Booking.STATUS.completed, bookings__pickup_time__year=year, bookings__pickup_time__month=month) & ~Q(bookings__status=Booking.STATUS.completed, bookings__pickup_time__lt=date(year, month, 1)) ).distinct().values_list('id', flat=True)
但测试用例执行失败:
def test_get_first_time_customer_ids(self) -> None: customer_1 = Customer.objects.create(name="Customer 1") customer_2 = Customer.objects.create(name="Customer 2") Booking.objects.bulk_create([ Booking(number="A", customer=customer_1, price=100_000, status=Booking.STATUS.completed, pickup_time=dt(2023, 2, 4, 12), route_id=1, vehicle_category_id=1), Booking(number="B", customer=customer_1, price=100_000, status=Booking.STATUS.completed, pickup_time=dt(2023, 1, 5, 12), route_id=1, vehicle_category_id=1), Booking(number="E", customer=customer_2, price=100_000, status=Booking.STATUS.completed, pickup_time=dt(2023, 2, 10, 12), route_id=1, vehicle_category_id=1) ]) ids = get_first_time_customer_ids(2023, 2) self.assertTrue(customer_2.id in ids) self.assertFalse(customer_1.id in ids)
预期customer_1不应出现在2023年2月的首次预订客户列表中,但实际被包含,请求排查问题。
问题原因
你的Q查询逻辑存在漏洞:当客户同时拥有指定年月的完成预订和更早的完成预订时,Django的关联查询会将客户匹配到第一个Q条件(存在2月的完成预订),而第二个~Q条件的判断逻辑并不是“该客户没有任何更早的完成预订”——实际上,这个写法会生成两次独立的关联查询(JOIN),只要存在一个预订满足第一个Q,且存在一个预订不满足第二个Q的条件(比如2月的预订本身就不早于2月1日),就会保留该客户,这完全不符合“首次预订在指定年月”的需求。
解决方案
正确的思路是:先为每个客户计算其最早的完成预订时间,再筛选这个最早时间落在指定年月内的客户。可以通过annotate结合Min聚合函数实现:
from django.db.models import Min, Q from datetime import date def get_first_time_customer_ids(year: int, month: int) -> QuerySet: # 计算指定年月的起始和结束日期 start_date = date(year, month, 1) # 获取下一个月的第一天,作为结束边界(不包含) if month == 12: end_date = date(year + 1, 1, 1) else: end_date = date(year, month + 1, 1) return Customer.objects.annotate( first_completed_booking=Min( 'bookings__pickup_time', filter=Q(bookings__status=Booking.STATUS.completed) ) ).filter( first_completed_booking__gte=start_date, first_completed_booking__lt=end_date ).values_list('id', flat=True)
逻辑说明:
annotate(first_completed_booking=Min(...)):为每个客户计算其所有完成状态预订的最早取车时间。filter条件确保这个最早时间落在指定年月的区间内(start_date <= 时间 < end_date),这样就能精准筛选出首次完成预订在该年月的客户。- 这种写法避免了多关联查询的逻辑漏洞,结果准确且性能更优。
内容的提问来源于stack exchange,提问作者Andi Fathul Mukminin
相关产品推荐
相关产品推荐

