# 46.8.显式子转换

46.8.1. 子事务上下文管理器

46.8.2. 较旧的Python版本

从数据库访问导致的错误中恢复,如中所述第46.7.2节可能会导致一种不希望出现的情况,即某些操作在其中一个操作失败之前成功,而在从该错误中恢复后,数据会处于不一致的状态。PL/Python以显式子事务的形式提供了这个问题的解决方案。

# 46.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 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;

如果第二个使现代化语句导致引发异常,此函数将报告错误,但使现代化尽管如此,我们仍将继续努力。换句话说,资金将从乔的账户中提取,但不会转入玛丽的账户。

为了避免此类问题,您可以将plpy。处决在显式子事务中调用。这个plpy模块提供了一个助手对象来管理使用plpy。子交易()作用此函数创建的对象实现上下文管理器接口 (opens new window).使用显式子转换,我们可以将函数重写为:

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;

请注意尝试/抓住仍然需要。否则,异常将传播到Python堆栈的顶部,并导致整个函数因PostgreSQL错误而中止,因此操作表中不会插入任何行。subtransaction上下文管理器不会捕获错误,它只确保在其作用域内执行的所有数据库操作都将被原子提交或回滚。子事务块的回滚发生在任何类型的异常出口上,而不仅仅是由来自数据库访问的错误引起的异常出口。在显式子事务块中引发的常规Python异常也会导致子事务回滚。

# 46.8.2.较旧的Python版本

上下文管理器语法使用具有关键字在Python 2.6中默认可用。为了与较早的Python版本兼容,可以调用subtransaction manager的__进入____退出__使用进来出口方便的别名。转移资金的示例函数可以写成:

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;