Django中save()后字段偶现空白问题及排查无效求助
我有一个Django GenericViewSet的POST端点,逻辑是根据请求里的ID找到对应的CarObj实例,然后设置指定字段值:
obj = models.CarObj.objects.get(id=self.request.data['id']) if self.request.data.get('field_value',False): obj.field = self.request.data['field_value'] obj.save()
这段代码95%的情况都正常,但偶尔会出现明明端点被正确触发,obj.field却变成空白的情况。
为了排查问题,我修改了代码,在保存后重新从数据库拉取实例并对比:
obj = models.CarObj.objects.get(id=self.request.data['id']) if self.request.data.get('field_value',False): obj.field = self.request.data['field_value'] obj.save() obj2 = models.CarObj.objects.get(id=obj.id) print("Mem: {} | DB: {}".format(obj.field, obj2.field)) if obj2.field == '' or len(obj2.field) == 0: raise ValueError("Mem does not match DB")
结果打印出来的内存实例和数据库实例的field值始终一致且正确,也从未触发ValueError,但field还是偶尔会变成空白。
之后我检查了CarObj的ModelViewSet,确保所有POST/PATCH/PUT请求在序列化器处理前都执行了self.request.data.pop('field_value'),问题依旧。
我又添加了pre_save信号来阻止字段被置空:
@receiver(pre_save, sender=models.CarObj) def carobj_pre_save(sender, **kwargs): instance = kwargs.get('instance') if 'field' in instance.get_dirty_fields(check_relationship=True): field_new_value = getattr(instance,'field','') if field_new_value == '' or len(field_new_value) == 0: try: old_instance = models.CarObj.objects.get(id=instance.id) setattr(instance, 'field', getattr(old_instance, 'field', '')) except models.CarObj.DoesNotExist: pass
理论上这个信号应该能阻止数据库字段被设为空,但问题还是偶尔发生,打印结果依然显示内存和数据库值一致。这到底是什么原因?
1. 并发写操作冲突(最可能原因)
你的代码是典型的读取-修改-写入模式,在高并发场景下会出现竞态条件:
- 比如请求A刚读取实例并修改,还没保存时,请求B也读取了同一个旧实例;
- 请求A先保存成功,随后请求B的保存操作会直接覆盖请求A的修改;
- 而你在当前请求里保存后立即拉取实例,只能看到自己写入后的结果,看不到后续其他请求的覆盖操作。
解决办法:加行锁避免并发冲突
用select_for_update()在读取实例时给数据库行加锁,确保同一时间只有一个请求能修改该实例:
from django.db import transaction with transaction.atomic(): # 加行锁,其他请求必须等当前事务结束才能读取该实例 obj = models.CarObj.objects.select_for_update().get(id=self.request.data['id']) if self.request.data.get('field_value', False): obj.field = self.request.data['field_value'] obj.save()
2. 信号逻辑存在漏洞
你写的pre_save信号有几个问题:
get_dirty_fields的检测可能不准确,某些场景下字段被修改但没被标记为脏字段;- 信号里读取
old_instance同样存在并发问题,可能拿到的不是最新数据; - 如果其他修改操作没有触发
field的脏字段检测,信号就不会生效。
优化信号逻辑:
添加详细日志,同时改用更可靠的方式获取字段旧值(需安装django-dirtyfields):
import logging from django.db.models.signals import pre_save from django.dispatch import receiver from myapp.models import CarObj logger = logging.getLogger(__name__) @receiver(pre_save, sender=CarObj) def carobj_pre_save(sender, **kwargs): instance = kwargs['instance'] # 检查field是否被修改 if instance._state.adding is False and instance.has_changed('field'): old_value = instance.get_dirty_fields()['field'] new_value = instance.field logger.info(f"CarObj[{instance.id}] field变更:旧值='{old_value}',新值='{new_value}'") if not new_value.strip(): # 尝试获取最新的数据库值恢复 try: db_instance = CarObj.objects.get(id=instance.id) instance.field = db_instance.field logger.warning(f"阻止CarObj[{instance.id}] field被置空,恢复为'{db_instance.field}'") except CarObj.DoesNotExist: pass
3. 排查隐性修改路径
虽然你已经在请求处理前pop了field_value,但可能存在其他地方偷偷修改field:
- 搜索项目中所有
CarObj.objects.update()、obj.field =、setattr(obj, 'field', ...)的代码; - 检查其他信号(比如post_save)、全局中间件、序列化器的
update方法是否有修改field的逻辑; - 排查是否有定时任务、异步任务在修改该字段。
4. 增强日志追溯
在所有修改field的地方添加日志,记录实例ID、操作人、新旧值、请求ID等信息,下次出现问题时可以直接定位到触发源。
内容的提问来源于stack exchange,提问作者savvamadar

