Django Model重写save方法及update批量更新踩坑详解
本文详解Django中重写Model的save方法以实现入库前数据校验的最佳实践,并深入剖析使用QuerySet的update方法进行批量更新时不会触发save、pre_save及post_save信号的常见陷阱。通过对比代码示例,提供确保调用save方法的解决方案,并补充说明save对未修改字段的覆盖行为,帮助开发者避免数据一致性问题。
这是一个非常实用的开发技巧。
在Django开发中,若需对存入数据库的数据进行校验,通常有哪些实现路径?
一种直观的做法是在View层处理:每次接收请求时,先对提交数据进行校验,若校验失败直接返回错误信息,不执行数据库写入;若校验通过,再调用create或update方法将数据持久化。
上述方式逻辑简单且易于理解,但随之带来的问题是,必须在所有接收数据的接口处重复编写校验逻辑。是否存在更优雅的方案?参考前文《Django使用Signals监测model字段变化发送通知》,可以利用Signals机制,注册pre_save信号。该信号在数据库数据变更前触发,在此处执行校验即可避免在View层多处重复校验。
然而,本文重点讨论另一种更为常用的方式:直接重写Model的save方法。
重写save方法实现数据校验
关于重写save方法的实现细节
save方法的核心职责是将对象持久化至数据库。若希望在数据入库前执行自定义处理逻辑,除Signals外,重写save方法是另一种有效手段。具体实现参考以下示例。
假设我们定义了如下Model结构:
class TempTask(models.Model):
...
exechost = models.CharField(max_length=64, default='localhost', verbose_name='执行主机')
execuser = models.ForeignKey(ExecUser, null=True, on_delete=models.PROTECT, db_constraint=False)
其中,exechost字段默认值为'localhost',execuser字段默认为空。现有业务需求要求:当exechost的值不为'localhost'时,其格式必须符合'ip:port'规范,且此时execuser字段不能为空。
针对此类较为复杂的校验逻辑,我们可以通过重写save方法来实现。
class TempTask(models.Model):
...
def save(self, *args, **kwargs):
if self.exechost and (self.exechost.strip() != 'localhost'):
if len(self.exechost.split(':')) != 2:
raise ValidationError('执行主机格式错误,应为ip:port格式')
if not self.execuser:
raise ValidationError('当执行主机存在时执行用户不能为空')
super().save(*args, **kwargs)
在save函数内部可以执行各种自定义逻辑,但必须注意:最后务必调用super().save()方法以执行父类的保存逻辑,否则数据无法真正写入数据库。
如此实现后,当执行create语句插入数据时,系统会先执行save方法中的校验逻辑。
示例代码如下: TempTask.objects.create(**postdata)
update批量更新的陷阱与解决
update批量更新的潜在问题
本以为一切顺利,准备休息时却发现:新增数据能正常触发校验,但更新数据时校验却未生效。相关更新代码如下:
示例代码: TempTask.objects.filter(id=pk).update(**postdata)
经过排查,问题根源在于官方文档中的明确说明。
官方指出: 'Unfortunately, there isn't a workaround when creating or updating objects in bulk, since none of save(), pre_save, and post_save are called.' (遗憾的是,当批量创建或更新对象时,由于不会调用save()、pre_save和post_save,因此没有变通方法。)
这意味着,使用QuerySet的update方法进行批量更新时,不会为每个对象调用save()方法,pre_save和post_save信号同样不会被触发。与此类似,Model的delete()方法在批量删除时也不会被调用,尽管此时仍可使用pre_delete或post_delete信号。
解决该问题的方案很简单,只需将更新代码调整为以下形式,以确保显式调用save方法:
_t = TempTask.objects.get(id=pk)
_t.__dict__.update(**postdata)
_t.save()
补充知识点:Django Model的save方法会对未更改的字段执行保存操作。
请参考以下代码逻辑:
obj = Obj.objects.get(id=1)
print obj.name #此时name的值假定为'abc'
def handler(oid):
obj = Obj.objects.get(id=oid)
obj.name = '123'
obj.save()
handler(obj.id)
obj.age = 10
obj.save()
print obj.name
最终name的值仍为'abc'。需要注意的是,save()在保存时,即使其他字段未修改,也会将内存中的当前值重新写入数据库。这意味着,若在内存中修改了其他字段但未调用save,而由子函数或其他进程直接修改数据库后,再次对原对象调用save(),其他进程更改的值会被覆盖。
以上关于Django Model重写save方法及update批量更新陷阱的详解即为全部内容,希望能对大家有所帮助,欢迎支持本站。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













