按第 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 as 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 as 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 异常也会导致子事务被回滚。
Python 2.6 中默认可用的是使用with
关键词的上下文管理器语法。为了与旧的 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 as 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;