Django一对多关系的注解、过滤与排序异常问题求助
我正在开发一个订单列表页面,支持按订单自身属性或关联模型属性进行过滤和排序。涉及的两个模型为Order和Block,二者是一对多关系:一个Order可关联零个或多个Block,一个Block必属于唯一的Order。
模型代码如下:
class Order(CustomBaseModel): date_of_order = models.DateField(default=timezone.now, verbose_name="Date of order") ... class Block(CustomBaseModel): ... order = models.ForeignKey(Order, on_delete=models.CASCADE) ...
为实现过滤有/无关联Block的订单,我对查询集做了注解:
order_queryset = Order.objects.all().annotate( is_material_available=Case( When(block__isnull=False, then=Value(True)), default=Value(False), output_field=BooleanField() ), )
随后基于该注解字段过滤:
is_material_available = self.data["is_material_available"] if is_material_available == "True": order_queryset = order_queryset.filter(is_material_available=True) elif is_material_available == "False": order_queryset = order_queryset.filter(is_material_available=False)
使用该方法出现异常:
- 当
is_material_available为"True"时:仅获取有Block的订单,但分页混乱(如每页应显示8条,实际可能仅1条,部分订单重复出现在不同页面); - 当
is_material_available为"False"时:获取所有订单(含有无Block的),但分页正常。
我尝试了多种过滤方式:
order_queryset = order_queryset.filter(Exists(Block.objects.filter(order=OuterRef("pk"))))
或
order_queryset = order_queryset.filter(block__isnull=False)
或
order_queryset = Order.objects.all().annotate( is_material_available=Count('block', distinct=True) ) order_queryset = order_queryset.filter(is_material_available__gt=0)
但结果一致,分页仍混乱。此外,用注解字段is_material_available排序无效,结果随机,尽管SQL查询看似正常。
请问问题出在哪里?
问题根源
过滤关联Block的订单时,Django会自动执行Order与Block的JOIN操作。如果一个Order关联多个Block,JOIN后的结果会生成多条重复的Order记录(每个Block对应一条)。分页是基于JOIN后的结果集计算的,这就导致:
- 单个订单被拆分成多条记录,分页时这些拆分后的记录会被当成独立条目,所以每页实际显示的唯一订单数量远少于预期;
- 重复的订单记录会出现在不同页面,因为它们在JOIN后的结果集中处于不同位置。
而过滤无Block的订单时,block__isnull=True只会匹配没有关联Block的订单,不会产生JOIN后的重复记录,因此分页正常。
解决办法
1. 对查询集去重
在过滤有Block的订单后,调用distinct()确保每个Order只出现一次:
if is_material_available == "True": order_queryset = order_queryset.filter(is_material_available=True).distinct()
使用Exists过滤时同样需要加distinct():
order_queryset = order_queryset.filter(Exists(Block.objects.filter(order=OuterRef("pk")))).distinct()
2. 统计关联数量时确保去重
用Count注解统计Block数量时,必须加上distinct=True避免重复计数,同时配合distinct()解决JOIN后的重复记录问题:
order_queryset = Order.objects.all().annotate( block_count=Count('block', distinct=True) ).filter(block_count__gt=0).distinct()
3. 追加唯一字段保证排序稳定
is_material_available只有两个取值,数据库对相同值的记录会随机排序。需要追加一个唯一排序字段(如id或date_of_order)来确保排序结果稳定:
order_queryset = order_queryset.order_by('is_material_available', '-id')
内容的提问来源于stack exchange,提问作者Quentin Merci

