如Section 46.7.2中所述,从数据库访问引发的错误中恢复,可能会造成一种不理想的情况:在某个操作失败之前,其他一些操作已经成功,而在从该错误恢复后,数据却处于不一致状态。PL/Python 以显式子事务的形式为这个问题提供了解决方案。
考虑以下实现两个账户之间转账的函数:
CREATE FUNCTION transfer_funds() RETURNS void AS $$
try:
plpy.execute("UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'")
plpy.execute("UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'")
except plpy.SPIError, e:
result = "error transferring funds: %s" % e.args
else:
result = "funds transferred correctly"
plan = plpy.prepare("INSERT INTO operations (result) VALUES ($1)", ["text"])
plpy.execute(plan, [result])
$$ LANGUAGE plpythonu;
如果第二条 UPDATE 语句引发异常,此函数会报告错误,但第一条 UPDATE 的结果仍会提交。换句话说,资金会从 Joe 的账户中扣除,却不会转入 Mary 的账户。
为避免此类问题,可以将 plpy.execute 调用放在显式子事务中。plpy 模块提供了用于管理显式子事务的辅助对象,可通过 plpy.subtransaction() 函数创建。此函数创建的对象实现了上下文管理器接口。使用显式子事务后,可以将函数改写为:
CREATE FUNCTION transfer_funds2() RETURNS void AS $$
try:
with plpy.subtransaction():
plpy.execute("UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'")
plpy.execute("UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'")
except plpy.SPIError, e:
result = "error transferring funds: %s" % e.args
else:
result = "funds transferred correctly"
plan = plpy.prepare("INSERT INTO operations (result) VALUES ($1)", ["text"])
plpy.execute(plan, [result])
$$ LANGUAGE plpythonu;
注意,仍需要使用 try/catch。否则,异常会传播到 Python 调用栈顶层,使整个函数因 PostgreSQL 错误而中止,从而不会向 operations 表插入任何行。子事务上下文管理器不会捕获错误,只保证在其作用域内执行的所有数据库操作以原子方式提交或回滚。任何异常退出都会使子事务块回滚,并不限于数据库访问错误。显式子事务块中抛出的普通 Python 异常,也会导致该子事务回滚。
使用 with 关键字的上下文管理器语法,从 Python 2.6 起默认可用。如果 PL/Python 使用更早的 Python 版本,仍然可以使用显式子事务,只是没那么方便。可以调用子事务管理器的 __enter__ 和 __exit__ 函数,也可以使用它们的便捷别名 enter 和 exit。转账示例函数可以写成:
CREATE FUNCTION transfer_funds_old() RETURNS void AS $$
try:
subxact = plpy.subtransaction()
subxact.enter()
try:
plpy.execute("UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'")
plpy.execute("UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'")
except:
import sys
subxact.exit(*sys.exc_info())
raise
else:
subxact.exit(None, None, None)
except plpy.SPIError, e:
result = "error transferring funds: %s" % e.args
else:
result = "funds transferred correctly"
plan = plpy.prepare("INSERT INTO operations (result) VALUES ($1)", ["text"])
plpy.execute(plan, [result])
$$ LANGUAGE plpythonu;
虽然 Python 2.5 已实现上下文管理器,但在该版本中使用 with 语法,需要使用 future 语句。不过,由于实现细节的限制,PL/Python 函数中不能使用 future 语句。