Django多对多模型创建时如何存储关联数据避免后续源数据变更影响
问题核心
原生Django的ManyToManyField仅存储关联模型的主键ID引用,不会留存关联时刻的字段副本,只要被关联的Two表数据后续发生修改/删除,查询First关联数据时拿到的永远是最新值,无法满足历史数据不随源数据变动的需求,必须调整模型结构实现数据快照能力,无法仅通过重写表单save方法实现。
实现方案
采用自定义多对多中间表存储关联快照的方案,在中间表中冗余存储需要留存的Two模型字段值,创建First关联时同步把当时Two的字段值拷贝到中间表,后续查询历史关联数据直接读取中间表的快照字段即可,完全不受Two源数据变动影响。
第一步:调整模型定义
# models.py from django.db import models class Two(models.Model): title = models.CharField(max_length=50, null=True) amount = models.PositiveIntegerField(null=True) class First(models.Model): # 百分比字段用DecimalField存储避免精度丢失,不要用IntegerField存小数 percent = models.DecimalField(max_digits=5, decimal_places=2) two = models.ManyToManyField( Two, blank=True, through="FirstTwoRelation", # 绑定自定义中间表 related_name="related_first" ) # 自定义多对多中间表,存储关联时刻的快照数据 class FirstTwoRelation(models.Model): first = models.ForeignKey(First, on_delete=models.CASCADE) # 源Two被删除时不影响快照留存,外键置空即可 two = models.ForeignKey(Two, on_delete=models.SET_NULL, null=True) # 快照字段,创建关联时同步拷贝Two对应字段的值 snapshot_title = models.CharField(max_length=50, null=True) snapshot_amount = models.PositiveIntegerField(null=True) create_time = models.DateTimeField(auto_now_add=True)
第二步:重写表单save逻辑
使用自定义中间表后不能调用默认的save_m2m()方法,需要手动创建中间表记录并写入快照值:
# forms.py from django import forms from .models import First, FirstTwoRelation class FirstAddForm(forms.ModelForm): class Meta: model = First fields = ["percent", "two"] # 显式声明表单字段 def save(self, commit=True): instance = super().save(commit=False) # 处理百分比值 instance.percent = self.cleaned_data["percent"] / 100 # 提前取出用户选中的Two对象列表 selected_two = list(self.cleaned_data["two"]) if commit: instance.save() # 清除旧关联(适配编辑场景,新增场景无影响) FirstTwoRelation.objects.filter(first=instance).delete() # 批量创建关联+写入快照 relation_objs = [] for two_item in selected_two: relation_objs.append(FirstTwoRelation( first=instance, two=two_item, snapshot_title=two_item.title, snapshot_amount=two_item.amount )) FirstTwoRelation.objects.bulk_create(relation_objs) return instance
第三步:调整视图查询逻辑
查询时直接读取中间表的快照字段,不要读取Two源表的字段,避免拿到修改后的新值:
# views.py from django.shortcuts import render from .models import First def FishDetailView(request, pk=None): # 预加载关联快照数据,避免N+1查询问题 first_queryset = First.objects.prefetch_related("firsttworelation_set").all() first_data = [] for first_obj in first_queryset: # 从中间表取快照数据 two_snapshot = list(first_obj.firsttworelation_set.all().values( "snapshot_title", "snapshot_amount", "two_id" )) first_data.append({ "percent": first_obj.percent, "two_list": two_snapshot }) context = { "first_list": first_data } return render(request, "fish_detail.html", context)
注意事项
- 不要采用“选中Two后克隆生成新Two对象再关联”的方案,会导致Two表堆积大量无业务意义的冗余历史数据,后续维护成本极高
- 后续如果
Two模型新增需要留存历史值的字段,同步在中间表加对应快照字段,创建关联时同步拷贝值即可 - 模板渲染时直接使用中间表的
snapshot_开头字段输出,不要通过first.two.all取源Two对象的字段,否则会拿到最新修改后的值 - 百分比字段如果需要做数值计算,优先用
DecimalField存储,不要用FloatField或者IntegerField存小数,避免精度丢失
内容的提问来源于stack exchange,提问作者farhad bidokhti
相关产品推荐
相关产品推荐

