
本文介绍如何在 django orm 中模拟 sql 的 group by 行为,通过 annotate() + concat 构建复合唯一键,对具有相同 address_from 和 address_to 的订单进行分组,并为每组统一关联 bill 实例。
本文介绍如何在 django orm 中模拟 sql 的 group by 行为,通过 annotate() + concat 构建复合唯一键,对具有相同 address_from 和 address_to 的订单进行分组,并为每组统一关联 bill 实例。
在 Django ORM 中,原生不支持直接调用 .group_by()(该方法不存在),也无法像 SQL 那样对多个外键字段进行分组聚合后迭代操作。但实际业务中常需“将具有相同收发货地址的订单归为一组,并批量分配同一账单(Bill)”,此时可通过注解(annotation)构造逻辑分组标识符来高效实现。
核心思路:用 Concat 生成虚拟分组键
利用 django.db.models.functions.Concat 将 address_from_id 与 address_to_id 拼接成唯一字符串(如 "123-456"),作为逻辑上的“分组主键”。该方式规避了 GROUP BY 缺失的限制,同时保持数据库层面的高效性(所有操作均可下推至 SQL 执行)。
以下是完整实现代码:
from django.db.models import CharField, Value
from django.db.models.functions import Concat
from myapp import models
# 步骤1:注解生成复合唯一键(格式:'address_from_id-address_to_id')
orders = models.Order.objects.annotate(
addresses_uk=Concat(
'address_from_id',
Value('-'),
'address_to_id',
output_field=CharField()
)
)
# 步骤2:提取所有唯一分组键
uk_set = set(orders.values_list('addresses_uk', flat=True))
# 步骤3:遍历每个分组,创建 Bill 并批量更新对应 Order
for uk in uk_set:
from_id, to_id = map(int, uk.split('-'))
# 创建新 Bill(假设 Bill 模型含 address_from/address_to 外键)
bill = models.Bill.objects.create(
address_from_id=from_id,
address_to_id=to_id,
# 其他必要字段(如 created_at、status 等)
)
# 批量更新:为该分组内所有 Order 关联此 Bill
orders.filter(addresses_uk=uk).update(bill=bill)
注意事项与优化建议
- ✅ 性能优先:全程使用 update() 而非逐条 .save(),避免 N+1 查询;filter().update() 是原子性 SQL UPDATE,高效且安全。
- ⚠️ 外键完整性:确保 address_from_id 和 address_to_id 在拼接前非空(可在 Order 模型中添加 null=False 约束,或在注解前加 filter(address_from__isnull=False, address_to__isnull=False))。
- ? 幂等性考虑:若需重复执行,建议先过滤 bill__isnull=True 的订单,或在 Bill 模型中添加唯一约束(如 unique_together = ('address_from', 'address_to')),防止重复创建。
- ? 扩展性提示:若后续需按更多字段分组(如加上 service_type),只需在 Concat 中追加参数即可,逻辑清晰可维护。
该方案兼顾可读性、性能与 Django 最佳实践,是替代原生 GROUP BY 的成熟工程解法。











