43.9. PL/Tcl中的显式子事务

第 43.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的结果将被提交。换句话说, 这些资金将从乔的账户中提取,但不会转移到玛丽的账户。 发生这种情况是因为每个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)不会导致回滚。