如 第 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
)不会导致回滚。