PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

42.9. PL/Tcl 中的显式子事务 #

如 Section 42.8 所述从数据库访问导致的 错误中恢复,可能导致一种不良情况:某个操作失败之前,一些操作 已经成功,而错误恢复之后数据会处于不一致的状态。 PL/Tcl 以显式子事务的形式为这个问题提供了解决方案。

考虑一个实现两个账户之间转账的函数:

CREATE FUNCTION transfer_funds() RETURNS void AS $$
    if [catch {
        spi_exec "UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'"
        spi_exec "UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'"
    } errormsg] {
        set result [format "error transferring funds: %s" $errormsg]
    } else {
        set result "funds transferred successfully"
    }
    spi_exec "INSERT INTO operations (result) VALUES ('[quote $result]')"
$$ LANGUAGE pltcl;

如果第二条 UPDATE 语句导致异常被抛出,该函数 会记录失败,但第一条 UPDATE 的结果仍然会被 提交。换句话说,资金会从 Joe 的账户中划出,但不会被转入 Mary 的账户。之所以发生这种情况,是因为每个 spi_exec 都是一个独立的子事务,而这些子事务 中只有一个被回滚了。

要处理这类情况,可以把多个数据库操作包装在一个显式子事务中, 它会作为一个整体成功或回滚。 PL/Tcl 提供了一个 subtransaction 命令来管理 这一点。我们可以把函数重写为:

CREATE FUNCTION transfer_funds2() RETURNS void AS $$
    if [catch {
        subtransaction {
            spi_exec "UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'"
            spi_exec "UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'"
        }
    } errormsg] {
        set result [format "error transferring funds: %s" $errormsg]
    } else {
        set result "funds transferred successfully"
    }
    spi_exec "INSERT INTO operations (result) VALUES ('[quote $result]')"
$$ LANGUAGE pltcl;

注意,为实现这一目的仍然需要使用 catch。 否则错误会传播到函数的顶层,阻止对 operations 表的期望插入。 subtransaction 命令不捕获错误,它只确保其 作用域内执行的所有数据库操作在报告错误时被一起回滚。

显式子事务的回滚发生在被包含的 Tcl 代码报告的任何错误上,不仅 限于源自数据库访问的错误。 因此,在 subtransaction 命令内部抛出的普通 Tcl 异常 同样会导致子事务回滚。 不过,从被包含 Tcl 代码的非错误退出(例如由于 return)不会导致回滚。