Django如何实现按外键分组排序的次级ID字段
Django按外键分组生成组内自增序号实现
预期效果
按外键字段分组,每组内的记录生成从1开始的连续递增序号,示例如下:
| id | secondary_id | 外键fk |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 1 |
| 3 | 3 | 1 |
| 4 | 1 | 2 |
| 5 | 2 | 2 |
业务场景:佣金结构CommissionStructure下关联多个佣金档位CommissionBracket,需要标记每个档位在所属佣金结构下的顺序编号。优先采用无冗余存储的动态计算方案,遵循DRY原则。
现有基础模型代码:
class CommissionStructure(APIBaseModel): advisor = models.ManyToManyField(AdviserDetail) name = models.CharField(max_length=20, blank=True, null=True, default='default') company = models.ForeignKey(Company, on_delete=models.CASCADE) start_dt = models.DateTimeField(auto_now_add=True) end_dt = models.DateTimeField(default=timezone.datetime.max) objects = CommissionStructureManager() class CommissionBracket(APIBaseModel): # 待实现secondary_id字段/逻辑 commission_structure = models.ForeignKey(CommissionStructure, on_delete=models.CASCADE, related_name="brackets") lower_bound = models.DecimalField(decimal_places=2, default=0.00, max_digits=20, null=True, blank=True) upper_bound = models.DecimalField(decimal_places=2, default=0.00, max_digits=20, null=True, blank=True)
方案1:QuerySet动态注解(推荐)
不需要在模型中持久化字段,通过数据库窗口函数动态计算分组序号,无数据一致性问题,完全符合DRY原则。
实现步骤
- 导入Django ORM窗口函数相关模块
from django.db.models import Window, F from django.db.models.functions import RowNumber - 为
CommissionBracket定义自定义管理器,全局注入序号注解逻辑,佣金档位默认按收入下限lower_bound升序排序,可根据业务需求调整排序字段:class CommissionBracketManager(models.Manager): def get_queryset(self): return super().get_queryset().annotate( secondary_id=Window( expression=RowNumber(), partition_by=F('commission_structure'), # 按外键分组 order_by=F('lower_bound').asc() # 组内排序规则 ) ) - 将管理器挂载到模型上即可,不需要额外定义
secondary_id字段:class CommissionBracket(APIBaseModel): commission_structure = models.ForeignKey(CommissionStructure, on_delete=models.CASCADE, related_name="brackets") lower_bound = models.DecimalField(decimal_places=2, default=0.00, max_digits=20, null=True, blank=True) upper_bound = models.DecimalField(decimal_places=2, default=0.00, max_digits=20, null=True, blank=True) objects = CommissionBracketManager()
使用方式
正常查询CommissionBracket实例即可直接访问secondary_id属性拿到组内序号,支持序列化输出、排序等常规操作:
# 示例:查询某个佣金结构下的所有档位,直接输出序号 brackets = CommissionStructure.objects.get(id=1).brackets.all() for bracket in brackets: print(bracket.secondary_id, bracket.lower_bound)
注意:该方案要求数据库支持窗口函数,目前MySQL 8.0+、PostgreSQL、SQLite 3.25+均原生支持,若使用低于8.0的MySQL版本可选择下方持久化方案。
方案2:持久化字段存储(兼容低版本数据库)
如果数据库版本不支持窗口函数,可将secondary_id作为实体字段存入表,通过重写save方法维护序号正确性。
实现代码
class CommissionBracket(APIBaseModel): secondary_id = models.PositiveIntegerField(verbose_name="组内序号") commission_structure = models.ForeignKey(CommissionStructure, on_delete=models.CASCADE, related_name="brackets") lower_bound = models.DecimalField(decimal_places=2, default=0.00, max_digits=20, null=True, blank=True) upper_bound = models.DecimalField(decimal_places=2, default=0.00, max_digits=20, null=True, blank=True) def save(self, *args, **kwargs): # 仅新建记录时自动生成序号,更新操作不修改原有序号 if not self.pk: current_max = self.__class__.objects.filter( commission_structure=self.commission_structure ).order_by('-secondary_id').values_list('secondary_id', flat=True).first() self.secondary_id = (current_max or 0) + 1 super().save(*args, **kwargs) class Meta: # 添加联合唯一约束,避免同一佣金结构下出现重复序号 constraints = [ models.UniqueConstraint( fields=['commission_structure', 'secondary_id'], name='unique_secondary_id_per_structure' ) ]
注意:该方案如果存在删除中间档位记录的场景,需要额外编写逻辑重排组内序号,维护成本高于动态注解方案。
内容的提问来源于stack exchange,提问作者Aetherovox
相关产品推荐
相关产品推荐

