Django框架ORM操作数据库不生效问题示例解决方法
作者:alex_i 发布时间:2024-01-23 17:38:34
本文详细描述使用Django 的ORM框架操作PostgreSQL数据库删除不生效问题的定位过程及解决方案,并总结使用ORM框架操作数据库不生效的问题的通用定位方法
问题描述
最近使用Django 的ORM框架操作PostgreSQL数据库总是出现删除不生效(尤其是在并发的时候)。业务代码中也没有任何报错。
定位过程 首先,我们怀疑是SQL语句拼装错误(比如ID不对),导致了删除不生效
通过在Python日志中打印ORM框架的SQL以及返回的操作结果,发现delete操作返回的记录数是1。且SQL中的ID符合业务逻辑,说明相应SQL语句是执行成功的。排除了这条猜测
接着我们怀疑DELETE操作后,数据又被其他业务CREATE回来了
通过在数据库中增加触发器,将nfinst表的写操作记录到nfinst_audit表,发现没有删除操作。排除了这条猜测
create table nfinst_audit(
operation char(1) not null,
stamp timestamp not null,
userid text not null,
nfinstid text not null,
order_id SERIAL ,
addr text not null,
port text not null
);
--将DELETE、UPDATE、INSERT操作记录到nfinst_audit表中
create or replace function process_nfinst_audit() returns trigger as $nfinst_audit$
begin
if (TG_OP = 'DELETE') then
insert into nfinst_audit(operation,stamp,userid,nfinstid,addr,port) VALUES('D',now(),user,old.nfinstid,inet_client_addr(),inet_client_port());
return old;
elsif (TG_OP = 'UPDATE') then
insert into nfinst_audit(operation,stamp,userid,nfinstid,addr,port) VALUES('U',now(),user,new.nfinstid,inet_client_addr(),inet_client_port());
return new;
elsif (TG_OP = 'INSERT') then
insert into nfinst_audit(operation,stamp,userid,nfinstid,addr,port) VALUES('I',now(),user,new.nfinstid,inet_client_addr(),inet_client_port());
return new;
end if;
return null;
end;
$nfinst_audit$ language plpgsql;
--创建触发器
create trigger nfinst_audit
before insert or update or delete on nfinst
for each row execute procedure process_nfinst_audit();
结合以上2点,猜测是事务没有commit导致
Django默认的事务模式是autocommit,每一次数据库操作执行后都会自动提交。项目使用的SQLAlchemy库的StaticPool连接池,配合gevent使用,一个进程中的所有协程串行复用一个数据库连接。
(这里解释一下为什么要一个进程中的所有协程复用一个连接,因为Python的PostgreSQL驱动pyscopg2是由c语言编写,协程在与数据库交互时,并不会因为io操作而切走,所以即使使用多个连接,也无法带来并发能力的提升,反而会增加维护多个连接的消耗)
查看delete操作的源码,delete操作是在一个事务中执行了pre_delete signal、删除表记录、post_delete signal等操作,执行完成后自动commit或者rollback。
def delete(self):
for model, instances in self.data.items():
self.data[model] = sorted(instances, key=attrgetter("pk"))
self.sort()
deleted_counter = Counter()
# 开启事务,语句块执行结束后会根据执行结果选择commit或者rollback
with transaction.atomic(using=self.using, savepoint=False):
for model, obj in self.instances_with_model():
if not model._meta.auto_created:
signals.pre_delete.send(
sender=model, instance=obj, using=self.using
)
for qs in self.fast_deletes:
count = qs._raw_delete(using=self.using)
deleted_counter[qs.model._meta.label] += count
for model, instances_for_fieldvalues in six.iteritems(self.field_updates):
query = sql.UpdateQuery(model)
for (field, value), instances in six.iteritems(instances_for_fieldvalues):
query.update_batch([obj.pk for obj in instances],
{field.name: value}, self.using)
for instances in six.itervalues(self.data):
instances.reverse()
for model, instances in six.iteritems(self.data):
query = sql.DeleteQuery(model)
pk_list = [obj.pk for obj in instances]
count = query.delete_batch(pk_list, self.using)
deleted_counter[model._meta.label] += count
if not model._meta.auto_created:
for obj in instances:
# 执行post_delete后置处理
signals.post_delete.send(
sender=model, instance=obj, using=self.using
)
这里的pre_delete signal跟post_delete signal类似于数据库的触发器,不过是在Python代码层面实现的。问题就出在这个post_delete signal上面,出错的数据表注册了post_delete signal,并在其中调用了REST接口,而调用REST接口会导致协程发生切换,如果切换后的协程也操作了数据库,会将现有的事务回滚。(因为从连接池新拿到的连接,应该保证是没有事务在执行的,如果有,就认为该连接上一次被使用时出现了异常,需回滚事务)
将post_delete相关逻辑注掉后,问题消失
解决方案
解决方法有如下几种:
直接修改Django源码,将post_delete signal的逻辑移除到事务外面(Django将post_delete的逻辑放在事务里确认有点坑,一旦post_delete出现异常就会导致事务回滚,并且事务过长也会消耗数据库资源)
修改业务代码,将delete成功后的处理逻辑由使用signal完成,改为重写Django Model的delete方法(先调用父类的delete方法,成功后再执行后置处理逻辑)
重写signal机制,post_delete使用自己实现的signal机制
最终综合考虑对业务代码的侵入性,以及后续的可维护性,我们选择了方案3来解决数据库删除不生效的问题。但在实施的时候,又发现了新的问题:django从数据库删除完数据后,会将Model对象也删除,从而导致post_delete无对象可操作。考虑到delete操作几乎不会出现rollback的情况,将post_delete移到了实际delete操作前面,类似于pre_delete。没有直接使用pre_delete是为了减少对业务代码的入侵。另外django自带的pre_delete也在事务中,而我们的改法是将signal操作移到事务外,以降低数据库压力
在models.py中做了如下修改
定义了自己的post_delete,并将业务代码中注册post_delete信号量改为从models.py导入post_delete变量
post_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True)
Django Model有2种方式进行删除操作,分别是直接对一条Model记录删除,以及对QuerySet进行删除。所以需要定义自己的Model类以及QuerySet基类,并让需要进行post_delete操作的Model类继承前面自定义的基类
class CModel_QuerySet(models.query.QuerySet):
def delete(self):
# 将post_delete信号量触发操作移到了事务外面
for inst in self:
post_delete.send(
sender=self.model, instance=inst, using=None
)
super(CModel_QuerySet, self).delete()
class CModel_CustomManager(models.Manager):
# custom QuerySet for snap QuerySet.update operations
def get_queryset(self):
return CModel_QuerySet(self.model, using=self._db)
# 自定义的Model基类
class CModelWithUpdateSignal(models.Model):
class Meta:
abstract = True
# custom models.Manager for snap QuerySet.update operations
objects = CModel_CustomManager()
def delete(self, *args, **kwargs):
# 将post_delete信号量触发操作移到了事务外面
post_delete.send(
sender=self.__class__, instance=self, using=None
)
super(CModelWithUpdateSignal, self).delete(*args, **kwargs)
# 需要进行post_delete操作的Model类
class NfInstModel(CModelWithUpdateSignal):
……
来源:https://blog.csdn.net/alex_i/article/details/128581367


猜你喜欢
- 解决MySql 数据库 提示:1045 access denied for user 'root'@'localho
- MySQL中union和order by是可以一起使用的,但是在使用中需要注意一些小问题,下面通过例子来说明。首先看下面的t1表。1、如果直
- Mcrypt扩展库可以实现加密解密功能,就是既能将明文加密,也可以密文还原。1.PHP加密扩展库Mcrypt安装在标准的PHP安装过程中并没
- 输入任意一个大写字母,生成金字塔图形def GoldTa(input): L = [chr(i) for i in range(
- 在用sqlAlchemy写web应用的时候,经常会用json进行通信,跟json最接近的对象就是dict,有时候操作dict也会比操作ORM
- 背景使用python操作一批同样分辨率的图片,合并为tiff格式的文件。由于opencv主要用于读取单帧的tiff文件,对多帧的文件支持并不
- requests相比urllib,第三方库requests更加简单人性化,是爬虫工作中常用的库requests安装初级爬虫的开始主要是使用r
- MySQL5.0版本的安装图解教程是给新手学习的,当前mysql5.0.96是最新的稳定版本。mysql 下载地址 https://www.
- 本文实例讲述了从ThinkPHP3.2.3过渡到ThinkPHP5.0学习笔记。分享给大家供大家参考,具体如下:用tp3.2.3做了不少项目
- 本文转自微信公众号:"算法与编程之美"一、前言三步搭建MUI页面主框架法包括新建含mui的HTML文件、输入mheade
- Photoshop Express,也就是传说中的web版photoshop,来了。和想象中的web photoshop相比,这个Photo
- 本文实例为大家分享了python实现图书借阅系统的具体代码,供大家参考,具体内容如下部分代码:from flask import Flask
- 上篇文章给大家介绍过 Python脚本破解Linux口令(crypt模块) 感兴趣的朋友点击查
- 今天因工作需要写了个小程序,用于在图片集中自动抽取需要的照片。该程序只是实现了基本功能,还有很多需要完善的地方,展示出来算是给自己鼓鼓气吧。
- 技术栈vue.js 主框架vuex 状态管理vue-router 路由管理一般过程在一般的登录过程中,一种前端方案是:检查状态:进入页面时或
- 1、旅行商问题(Travelling salesman problem, TSP)旅行商问题是经典的组合优化问题,要求找到遍历所有城市且每个
- 本文实例讲述了python使用any判断一个对象是否为空的方法。分享给大家供大家参考。具体实现代码如下:>>> eth =
- 如下所示:import socketimport threadingimport timedef testconn( host , port
- 上代码:#coding=utf-8import cv2import dlibpath = "imagePath/9.jpg&quo
- Python中的字典一、字典的特点二、创建字典创建字典用大括号表示dict1={'a':3,'b':4,