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

Django中save()后字段偶现空白问题及排查无效求助

问题:Django中CarObj字段偶尔被置空的诡异现象

我有一个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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 04:07:06