cx_Oracle和异常处理 - 好的实践?

32 投票
2 回答
73982 浏览
提问于 2025-04-17 02:33

我正在尝试使用 cx_Oracle 连接到一个 Oracle 实例,并执行一些 DDL 语句:

db = None
try:
    db = cx_Oracle.connect('username', 'password', 'hostname:port/SERVICENAME')
#print(db.version)
except cx_Oracle.DatabaseError as e:
    error, = e.args
    if error.code == 1017:
        print('Please check your credentials.')
        # sys.exit()?
    else:
        print('Database connection error: %s'.format(e))
cursor = db.cursor()
try:
    cursor.execute(ddl_statements)
except cx_Oracle.DatabaseError as e:
    error, = e.args
    if error.code == 955:
        print('Table already exists')
    if error.code == 1031:
        print("Insufficient privileges - are you sure you're using the owner account?")
    print(error.code)
    print(error.message)
    print(error.context)
cursor.close()
db.commit()
db.close()

不过,我不太确定在这里处理异常的最佳设计是什么。

首先,我在一个尝试的代码块里创建了 db 对象,以便捕捉任何连接错误。

但是,如果无法连接,那么在后面的代码中 db 就不存在了——这就是我在上面设置 db = None 的原因。不过,这样做算不算好习惯呢?

理想情况下,我需要先捕捉连接错误,然后是执行 DDL 语句时的错误,依此类推。

把异常嵌套处理是个好主意吗?还是说有更好的方法来处理这种相互依赖的异常呢?

另外,有些情况(比如连接失败)我希望脚本直接终止——所以我注释掉了 sys.exit() 的调用。不过,我听说像这样用异常处理来控制流程是不好的习惯。你怎么看?

2 个回答

1

另一种可能更优雅的解决方案是给你的数据库调用函数加上一个装饰器。这个装饰器可以帮助你处理错误,并重新尝试数据库的调用。如果连接过期了,解决方法就是重新连接并重新发起请求。

下面是我用过的装饰器:

####### Decorator named dbReconnect ########
#Retry decorator
#Retries a database function twice when  the 1st fails on a stale connection
def dbReconnect():
    def real_decorator(function):
        def wrapper(*args, **kwargs):
            try:
                return function(*args, **kwargs)
            except  Exception as inst:
                print ("DB error({0}):".format(inst))
                print ("Reconnecting")
                #...Code for reconnection is to be placed here..
                ......
                #..end of code for reconnection
            return function(*args, **kwargs)
        return wrapper
    return real_decorator

###### Decorate the DB Call like this: #####
    @dbReconnect()
    def DB_FcnCall(...):
    ....

更多细节请查看Github: https://github.com/vvaradarajan/DecoratorForDBReconnect/wiki

注意:如果你使用连接池,内部的连接池技术会检查连接,如果连接过期了,它也会自动刷新,这样也能解决这个问题。

38

不过,如果连接不上数据库,那么后面就无法使用 db 了——这就是我在上面设置 db = None 的原因。但是,这样做算不算好习惯呢?

不,设置 db = None 并不是最佳做法。连接数据库有两种可能,要么成功,要么失败。

  • 连接数据库失败:

    当捕获到异常后,如果没有重新抛出异常,你会继续执行,直到到达 cursor = db.Cursor()

    此时 db == None,所以会抛出一个类似于 TypeError: 'NoneType' object has no attribute 'Cursor' 的异常。因为数据库连接失败时产生的异常已经被捕获,所以失败的原因就被隐藏了。

    我个人认为,除非你打算很快再试一次,否则我总是会重新抛出连接异常。你怎么捕获这个异常由你决定;如果错误持续存在,我会发邮件提醒“去检查一下数据库”。

  • 连接数据库成功:

    在你的 try:... except 块中,变量 db 会被赋值。如果 connect 方法成功执行,那么 db 就会被连接对象替代。

无论如何,db 的初始值都不会被使用。

不过,我听说像这样用异常处理来控制流程是不好的做法。

与其他语言不同,Python 确实使用异常处理来控制流程。在我回答的最后,我链接了几个在 Stack Overflow 和 Programmers 上提出类似问题的帖子。在每个例子中,你都会看到“但在 Python 中”这句话。

这并不是说你应该过度使用,但 Python 通常遵循的原则是 EAFP,也就是“请求宽恕比请求许可更容易”。在 如何检查变量是否存在? 的三个最高投票例子很好地展示了你可以选择使用或不使用流程控制。

嵌套异常是个好主意吗?还是有更好的方法来处理这种依赖/级联异常?

嵌套异常没有什么问题,只要你合理使用。考虑一下你的代码。你可以去掉所有异常,把整个代码包裹在一个 try:... except 块中。如果抛出异常,你就知道是什么,但要准确找出问题所在会有点困难。

那么,如果你想在 cursor.execute 失败时给自己发邮件呢?你应该在 cursor.execute 周围放一个异常处理,以便执行这个任务。然后你重新抛出异常,这样它就会被外层的 try:... 捕获。如果不重新抛出,代码会继续执行,就像什么都没发生一样,而你在外层 try:... 中处理异常的逻辑会被忽略。

最终,所有异常都是从 BaseException 继承而来的。

另外,有些地方(例如连接失败)我希望脚本直接终止——因此我注释掉了 sys.exit() 的调用。

我添加了一个简单的类和如何调用它,这大致是我会做的事情。如果这个脚本要在后台运行,那么打印错误信息就没有意义——人们不会坐在那里手动查看错误。错误应该以你标准的方式记录下来,并通知相关人员。出于这个原因,我去掉了打印,改为提醒记录。

由于我将类拆分成多个函数,当 connect 方法失败并抛出异常时,execute 调用将不会执行,脚本会在尝试断开连接后结束。

import cx_Oracle

class Oracle(object):

    def connect(self, username, password, hostname, port, servicename):
        """ Connect to the database. """

        try:
            self.db = cx_Oracle.connect(username, password
                                , hostname + ':' + port + '/' + servicename)
        except cx_Oracle.DatabaseError as e:
            # Log error as appropriate
            raise

        # If the database connection succeeded create the cursor
        # we-re going to use.
        self.cursor = self.db.cursor()

    def disconnect(self):
        """
        Disconnect from the database. If this fails, for instance
        if the connection instance doesn't exist, ignore the exception.
        """

        try:
            self.cursor.close()
            self.db.close()
        except cx_Oracle.DatabaseError:
            pass

    def execute(self, sql, bindvars=None, commit=False):
        """
        Execute whatever SQL statements are passed to the method;
        commit if specified. Do not specify fetchall() in here as
        the SQL statement may not be a select.
        bindvars is a dictionary of variables you pass to execute.
        """

        try:
            self.cursor.execute(sql, bindvars)
        except cx_Oracle.DatabaseError as e:
            # Log error as appropriate
            raise

        # Only commit if it-s necessary.
        if commit:
            self.db.commit()

然后调用它:

if __name__ == "__main__":

    oracle = Oracle.connect('username', 'password', 'hostname'
                           , 'port', 'servicename')

    try:
        # No commit as you don-t need to commit DDL.
        oracle.execute('ddl_statements')

    # Ensure that we always disconnect from the database to avoid
    # ORA-00018: Maximum number of sessions exceeded. 
    finally:
        oracle.disconnect()

进一步阅读:

cx_Oracle 文档

为什么不把异常当作常规流程控制?
Python 的异常处理比 PHP 和其他语言更高效吗?
使用 try catch 作为逻辑运算符的理由

撰写回答