亲宝软件园·资讯

展开

Django框架ORM操作数据库不生效问题示例解决方法

alex_i 人气:0

本文详细描述使用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();

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相关逻辑注掉后,问题消失

解决方案

解决方法有如下几种:

最终综合考虑对业务代码的侵入性,以及后续的可维护性,我们选择了方案3来解决数据库删除不生效的问题。但在实施的时候,又发现了新的问题:django从数据库删除完数据后,会将Model对象也删除,从而导致post_delete无对象可操作。考虑到delete操作几乎不会出现rollback的情况,将post_delete移到了实际delete操作前面,类似于pre_delete。没有直接使用pre_delete是为了减少对业务代码的入侵。另外django自带的pre_delete也在事务中,而我们的改法是将signal操作移到事务外,以降低数据库压力

在models.py中做了如下修改

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):
    ……

总结

ORM框架操作数据库,可以抽象至如下流程

如果出现操作数据库不生效,但是也没有报错的情况。可以从以下几个方面来排查问题 

是否SQL本身执行未生效(通常是业务逻辑导致,比如DELETE操作传错了ID),可以在ORM框架源码中加日志,将SQL执行结果打印出来
是否本次操作被其他操作覆盖,可以对数据表增加触发器,将CREATE、UPDATE、DELETE操作记录到另一张表。通过查看操作记录来确认是否是业务逻辑覆盖的问题
是否是事务没有COMMIT,可以在ORM框架源码中COMMIT操作前后增加日志,如发现确实没有COMMIT,需要排查在事务执行过程(包含前置signal、执行SQL、后置signal等处理)中,是否出现异常,以及数据库连接在中途有没有被其他线程使用

加载全部内容

相关教程
猜你喜欢
用户评论