42.8で説明されているように、データベースアクセスによって生じたエラーからの回復により、操作のうちいくつかが失敗する前に他の操作が成功し、エラーからの回復後、データの一貫性が失われた望ましくない状態になってしまう可能性があります。 PL/Tclは明示的なトランザクションの手法でこの問題を解決する手段を提供しています。
2つのアカウントの間の送金を実装する関数を考えます。
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によるもの)は、ロールバックをもたらしません。