You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django如何实现按外键分组排序的次级ID字段

Django按外键分组生成组内自增序号实现

预期效果

按外键字段分组,每组内的记录生成从1开始的连续递增序号,示例如下:

idsecondary_id外键fk
111
221
331
412
522

业务场景:佣金结构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原则。

实现步骤

  1. 导入Django ORM窗口函数相关模块
    from django.db.models import Window, F
    from django.db.models.functions import RowNumber
    
  2. 为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()  # 组内排序规则
                )
            )
    
  3. 将管理器挂载到模型上即可,不需要额外定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 11:12:05