43.8. 明确的子事务

第 43.7.2 节中描述的数据库访问导致的错误中恢复可能会导致不良情况, 在一个操作失败之前已经有一些操作成功了,之后从错误中恢复使得数据停留在一个不一致的状态。 PL/Python提供了一个解决该问题的方法,以明确的子事务的形式。

43.8.1. 子事务内容管理器

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

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的结果将不会提交。换句话说,funds将从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异常也会导致子事务回滚。

43.8.2. 旧的Python版本

使用with关键字的内容管理器语法缺省在Python 2.6中可用。 如果用旧的Python版本使用PL/Python,仍然可能使用明确的子事务,尽管不是透明的。 你可以使用方便的别名enterexit 调用子事务管理器的__enter____exit__。 转换funds的例子函数可以写为:

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 语法你需要使用一个未来声明。 不过,由于实现的细节,你不能在PL/Python函数中使用未来声明。