如何在可删除无效记录的inline_formset中使用ExclusionConstraint
解决方案
问题根因
Django inline_formset 的默认执行顺序如下:
- 逐一对每个表单关联的模型实例调用
clean()方法 - 处理标记为删除的实例,执行数据库删除操作
- 保存剩余的新增/更新实例
模型层的clean方法无法感知当前表单集中哪些实例是即将被删除的,因此校验时会把待删除的记录也纳入重叠判断,导致误报。
推荐方案:将重叠校验迁移至FormSet全局clean方法
将原本在模型层的重叠校验逻辑上移到FormSet层,校验时主动跳过待删除的实例,同时支持表单集内实例的交叉校验:
- 自定义InlineFormSet
from django.forms import BaseInlineFormSet from django.core.exceptions import ValidationError class MyModelInlineFormSet(BaseInlineFormSet): def clean(self): super().clean() # 已有单字段校验错误的话跳过全局校验 if any(self.errors): return valid_instances = [] # 收集所有表单集内的实例ID,用于排除校验 formset_instance_ids = [ form.instance.id for form in self.forms if form.instance.id is not None ] for form in self.forms: # 跳过标记为删除的实例 if self.can_delete and self._should_delete_form(form): continue current_instance = form.instance current_range = current_instance.date_range # 1. 校验和当前表单集内其他未删除实例是否重叠 for saved_range, saved_form in valid_instances: if current_range.overlaps(saved_range): form.add_error( 'date_range', '日期范围不能和同组内其他未删除的记录重叠' ) saved_form.add_error( 'date_range', '日期范围不能和同组内其他未删除的记录重叠' ) # 2. 校验和表单集外的数据库已有记录是否重叠 db_conflict = MyModel.objects.exclude( id__in=formset_instance_ids ).filter(date_range__overlap=current_range).exists() if db_conflict: form.add_error( 'date_range', '日期范围不能和系统已有记录重叠' ) valid_instances.append((current_range, form))
- 调整模型层clean方法
如果其他非表单集场景仍然需要模型层的重叠校验,可以给clean方法增加跳过参数:
def clean(self, skip_overlap_check=False): error_dict = {} if not skip_overlap_check: if MyModel.objects.exclude(id=self.id).filter( date_range__overlap=self.date_range ).exists(): error_dict['date_range'] = ValidationError( 'Range can not overlap with an existing period.', code='overlap_period' ) if error_dict: raise ValidationError(error_dict)
- 绑定自定义FormSet到你的Inline配置
以Django Admin为例:
from django.contrib import admin from .models import MyModel, ParentModel class MyModelInline(admin.TabularInline): model = MyModel formset = MyModelInlineFormSet # 其他Inline配置 @admin.register(ParentModel) class ParentModelAdmin(admin.ModelAdmin): inlines = [MyModelInline]
备选方案:调整表单集保存顺序
如果不想修改校验逻辑,可以重写表单集保存方法,先执行删除操作再做校验和保存:
from django.db import transaction from django.contrib import admin @admin.register(ParentModel) class ParentModelAdmin(admin.ModelAdmin): inlines = [MyModelInline] @transaction.atomic def save_formset(self, request, form, formset, change): # 先删除标记为删除的实例 for obj in formset.deleted_objects: obj.delete() # 再保存剩余实例,此时校验不会包含已删除的记录 formset.save()
注意:无论用哪种方案,都请保留数据库层的排除约束,作为并发写入场景下的最后一道数据一致性防线。
内容的提问来源于stack exchange,提问作者alias51
相关产品推荐
相关产品推荐

