如何嵌套方法以在 Python 中安全地执行 SQLite 事务?

How to nest methods to execute a SQLite transaction safely in Python?

我正在 class 创建 Python 以及一些 SQLite 测试,我想知道我处理连接的方式是否正确(最重要的是,安全) .

在 class Test 中,我有两种方法:simple_operation,在 nested set model table 中插入一个新值,以及 coupled_operation,连续执行两个simple_operation以避免冗余。

目前,所有其他涉及对数据库进行任何插入或更新的方法都会打开连接并在完成操作后关闭它,例如 simple_operation 最初计划:

class Test:
    def simple_operation(self, value):
        conn = sqlite3.connect(mydatabase)
        cur = conn.cursor()
        with conn:
            cur.execute('UPDATE nested SET rgt = rgt + 2 WHERE rgt >= ?;', (value,))
            cur.execute('UPDATE nested SET lft = lft + 2 WHERE lft >= ?;', (value,))
            cur.execute('INSERT INTO nested(lft, rgt) VALUES(?, ?);', (value, value+1))

        conn.close()

但是,由于 coupled_operation 将执行它两次,并且其目的是将两个具有共同点的简单操作配对,因此它会有自己的事务返回到简单方法:

class Test:
    def coupled_operation(self, value):
        connd = sqlite3.connect(mydatabase)
        with connd:
            self.simple_operation(value)
            self.simple_operation(value+2)
        conn.close()

然而,由于 simple_operation 实现了它自己的事务,以相同的方式处理它会提交它所做的每个 simple_operation,违背了 connd 作为上下文管理器的目的。在实际层面上,此实现执行 2x conn.commit,每个执行 3x cur.execute,而不是先执行 6x cur.execute,然后执行 connd.commit.

我的解决方案目前正在通过将光标作为可选参数留在 simple_operation 中,无论是否提供都改变行为:

class Test:
    def coupled_operation(self, value):
        connd = sqlite3.connect(mydatabase)
        curd = connd.cursor()
        with connd:
            self.simple_operation(value, curd)
            self.simple_operation(value+2, curd) # A

        conn.close()

    def simple_operation(self, value, outer_cursor=None):
        if outer_cursor is None:
            conn = sqlite3.connect(mydatabase)
            cur = conn.cursor()
            with conn:
                cur.execute('UPDATE nested SET rgt = rgt + 2 WHERE rgt >= ?;', (value,))
                cur.execute('UPDATE nested SET lft = lft + 2 WHERE lft >= ?;', (value,))
                cur.execute('INSERT INTO nested(lft, rgt) VALUES(?, ?);', (value, value+1))

            connd.close()
        else:
            # Executes, but doesn't commit, leaves for coupled_operation to integrate into transaction
            outer_cursor.execute('UPDATE nested SET rgt = rgt + 2 WHERE rgt >= ?;', (value,))
            outer_cursor.execute('UPDATE nested SET lft = lft + 2 WHERE lft >= ?;', (value,))
            outer_cursor.execute('INSERT INTO nested(lft, rgt) VALUES(?, ?);', (value, value+1)) # B

但是,我不确定,如果以这种方式完成,它是否成功包含在所需的事务中,这样 如果 # B 来自 # A 整个操作将被回滚。


出于好奇,我每次处理数据库时都会打开和关闭连接。以前我将单个连接设置为 class 属性 并开始使用其他方法,但在实例化 class 时我最终锁定了我的数据库。你们认为这种方式还是示例中显示的方式更有效?

当然,最简单的解决方案是将 simple_operation() 函数的关注点分为提交事务的部分和执行实际工作的部分:

class Test:
    def __init__(self, path):
        self.conn = sqlite3.connect(path)

    def coupled_operation(self, value):
        with self.conn:
            cursor = self.conn.cursor()
            self.do_simple_operation(cursor, value)
            self.do_simple_operation(cursor, value+2)

    def simple_operation(self, value):
        with self.conn:
            cursor = self.conn.cursor()
            self.do_simple_operation(cursor, value)

    def do_simple_operation(self, cursor, value):
        cursor.execute('UPDATE nested SET rgt = rgt + 2 WHERE rgt >= ?;', (value,))
        cursor.execute('UPDATE nested SET lft = lft + 2 WHERE lft >= ?;', (value,))
        cursor.execute('INSERT INTO nested(lft, rgt) VALUES(?, ?);', (value, value+1))

这应该可以解决问题。没有必要一直关闭和重新打开数据库,这只会导致速度变慢。我不确定您为什么会遇到锁定问题;也许你在线程之间共享这个 class 的对象?这在 SQLite 中是不允许的。

为了安全起见,您可能会以这种方式拆分所有方法。所有方法foo()只打开一个连接上下文管理器并调用do_foo(),而以do_开头的方法可以自由调用。

以下是一些可能有用的额外注释:

  • 使用同一个游标对语句是否被认为是同一事务的一部分没有影响,对效率没有影响(Python sqlite3模块中有一个内部语句缓存但是它是每个连接而不是每个游标)所以你可以在每次执行语句时创建一个新游标。 sqlite3.Statement 对象上什至还有一个 execute() 方法,它是创建 Cursor 并在其上调用 execute() 的快捷方式(return 值是 Cursor 以备不时之需。
  • 以防万一你不知道(我当然认为这并不明显!),with conn 不启动事务,它只是提交(如果没有异常)或回滚(如果有异常)以某种方式启动的事务。如果当你到达 with conn 块的末尾时没有正在进行的交易,那么它会默默地什么也不做,所以你永远不会知道!
  • 默认情况下,Python sqlite3 模块会在执行某些语句时自动启动事务。规则很复杂,但请在您的示例中包含语句(但 而不是 包括例如 CREATE TABLE ...SELECT ...)。通过设置 isolation_mode=None 并手动启动事务来禁用这种令人困惑的逻辑可能是最安全的。
  • 我上面所说的关于游标和锁定的唯一轻微例外是,如果您执行 SELECT 语句但不消耗所有结果行并且不销毁或调用 close()Cursor 对象上,那么这可以将数据库锁扩展到事务结束之后(甚至超过 COMMIT,尽管如果事务锁是写锁,那么它会将其降级为读锁).只要您不在变量中长时间挂在 Cursor 对象上,这很少是一个实际问题。

通过这些调整,您的代码最终看起来像这样:

class Test:
    def __init__(self, path):
        self.conn = sqlite3.connect(path, isolation_mode=None)

    def coupled_operation(self, value):
        with self.conn:
            self.conn.execute("BEGIN")
            self.do_simple_operation(value)
            self.do_simple_operation(value+2)

    def simple_operation(self, value):
        with self.conn:
            self.conn.execute("BEGIN")
            self.do_simple_operation(value)

    def do_simple_operation(self, value):
        self.conn.execute('UPDATE nested SET rgt = rgt + 2 WHERE rgt >= ?;', (value,))
        self.conn.execute('UPDATE nested SET lft = lft + 2 WHERE lft >= ?;', (value,))
        self.conn.execute('INSERT INTO nested(lft, rgt) VALUES(?, ?);', (value, value+1))