Django批量修改实例:clean与save执行顺序及验证异常排查
Django Admin批量保存的流程与问题分析
核心问题解答
- Django Admin批量保存的默认工作流程:先对所有选中的实例依次执行clean验证,全部验证通过后,再逐个将实例保存到数据库。并非先清理单个实例并保存,再处理下一个的流程。
你遇到的现象原因
第一次批量设置多个实例的is_active=True时,数据库里原本没有该供应商的激活地址,每个实例执行clean方法时,查询SupplierAddress.objects.filter(supplier=self.supplier, is_active=True).exclude(id=self.id)的结果都是0,所以不会触发错误,所有实例都被保存到数据库。
第二次点击保存时,数据库里已经存在多个该供应商的激活地址了,此时每个实例的clean方法会查询到已有激活项,因此触发错误提示。
问题根源
你的clean方法是基于数据库当前状态做验证,但批量保存时,所有实例的验证步骤都在保存之前完成——第一个实例的修改还没写入数据库,后续实例的clean查询不到它的状态变化,导致批量操作时绕过了“单个供应商只能有一个激活地址”的限制。
解决方案
方案1:添加数据库级条件唯一约束(最可靠)
直接在模型的Meta.constraints里添加条件唯一约束,从数据库层面保证规则,即使绕过Admin的验证也无法违反:
class SupplierAddress(models.Model): """Model to create supplier's address instances""" class Meta: """Customize django default way to plural the class name""" verbose_name = 'Supplier Address' verbose_name_plural = 'Supplier Addresses' constraints = [ models.UniqueConstraint( fields=['supplier', 'address'], name='supplier_address_unique_appversion' ), # 新增条件唯一约束:每个supplier只能有一个is_active=True的地址 models.UniqueConstraint( fields=['supplier'], condition=models.Q(is_active=True), name='unique_active_supplier_address' ) ] # Define model fields. supplier = models.ForeignKey( 'Supplier', on_delete=models.CASCADE, related_name='supplier_addresses_supplier' ) address = models.ForeignKey( 'Address', on_delete=models.CASCADE, related_name='supplier_addresses_address' ) is_active = models.BooleanField(default=False) def clean(self): """Restrict the add/change to model fields""" if self.is_active is True: if SupplierAddress.objects.filter( supplier=self.supplier, is_active=True ).exclude(id=self.id).count() >= 1: raise forms.ValidationError( { "is_active": "You can't set more than one active address" } )
方案2:重写Admin的批量保存逻辑
在Admin类里重写批量处理方法,在保存前先做全局验证:
from django.contrib import admin from django import forms from .models import SupplierAddress class SupplierAddressAdmin(admin.ModelAdmin): # 可添加原有Admin配置,比如list_display等 def save_formset(self, request, form, formset, change): # 针对批量保存做全局验证 if formset.model == SupplierAddress: # 收集所有要设置为激活的实例 active_plans = [] for form_data in formset.cleaned_data: # 排除被删除的实例 if not form_data.get('DELETE', False) and form_data.get('is_active'): active_plans.append(form_data['supplier']) # 检查每个供应商的激活实例数量 supplier_count = {} for supplier in active_plans: supplier_count[supplier] = supplier_count.get(supplier, 0) + 1 if supplier_count[supplier] > 1: raise forms.ValidationError(f"供应商 {supplier} 只能设置一个激活地址") super().save_formset(request, form, formset, change) admin.site.register(SupplierAddress, SupplierAddressAdmin)
补充说明
数据库约束是最底层的保障,建议优先使用;Admin层面的验证可以作为用户友好的前置提示,避免用户操作后才触发数据库错误。
内容的提问来源于stack exchange,提问作者Wadhah Sky
相关产品推荐
相关产品推荐

