最近のデータベースシステムでは、ビジネスロジックはよくサーバーサイドで実装されるよ ― プロシージャや関数を使ってね。PostgreSQLを使うときは、関数とプロシージャの違い(特にバージョン11+でプロシージャが追加されたこと)と、それらがトランザクションとどう関わるかをちゃんと理解しておくのが大事だよ。
ここからは、PostgreSQL 17の公式ドキュメントと現時点での制限に基づいて、トランザクションの仕組み、ネストした呼び出し、プロシージャ/関数内での部分的なロールバックについて、基本的なポイントを説明するね。
キーポイント:関数 vs プロシージャ
関数(CREATE FUNCTION) ― ひとつの外部トランザクションの中で必ず実行される。関数の中では明示的なトランザクションコマンド(BEGIN、COMMIT、ROLLBACK、SAVEPOINT)は使えないよ。
- どんな変更も、外部トランザクションのレベルでしかコミットやロールバックされない。
- 関数内で「部分的なロールバック」をしたい場合は
BEGIN ... EXCEPTION ... ENDを使うけど、関数の中でコミットはできない。
プロシージャ(CREATE PROCEDURE) ― サーバーサイドで直接トランザクションを管理するために登場した(たとえば部分的なコミットや段階的なロールバックとか)。
- プロシージャ(PL/pgSQL)では
COMMIT、ROLLBACK、SAVEPOINT、RELEASE SAVEPOINTが使える。 - 重要:PL/pgSQLプロシージャ内で
ROLLBACK TO SAVEPOINTは使えない(シンタックスエラーになる)。 - プロシージャは
CALL ...というSQLコマンドでしか呼び出せない。SELECTや他の関数の中からは呼べないよ。
プロシージャや関数を他のプロシージャ/関数から呼ぶには?
関数は、普通に名前で呼び出して他の関数を「透過的に」呼べる:
-- 割引計算用の関数例
CREATE OR REPLACE FUNCTION calculate_discount(order_total NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
IF order_total >= 100 THEN
RETURN order_total * 0.1;
ELSE
RETURN 0;
END IF;
END;
$$ LANGUAGE plpgsql;
-- 注文処理用の関数が他の関数を呼び出す
CREATE OR REPLACE FUNCTION process_order(order_id INT, order_total NUMERIC)
RETURNS VOID AS $$
DECLARE
discount NUMERIC;
BEGIN
discount := calculate_discount(order_total);
RAISE NOTICE '割引: %', discount;
INSERT INTO orders_log (order_id, order_total, discount)
VALUES (order_id, order_total, discount);
END;
$$ LANGUAGE plpgsql;
すべてはひとつの外部トランザクションの中で実行される!どこかの関数でエラーが起きたら、全部の変更がロールバックされるよ。
プロシージャの呼び出しとネストしたトランザクション
プロシージャは他のプロシージャの中でCALL ...コマンドを使って呼び出せる(PostgreSQL 17ではCALL proc1() -> CALL proc2()みたいな呼び出しスタックもOK)。ただしトランザクションのルールは守られる:
- トランザクションコマンド(
COMMIT、ROLLBACK、SAVEPOINT、RELEASE SAVEPOINT)はプロシージャの最上位レベルでしか使えない。 - もしトランザクション管理をするプロシージャを、すでにアクティブな明示的トランザクションの中(たとえばautocommitなしのクライアントから)で呼び出すと、
COMMITやSAVEPOINTを実行しようとしたときにエラーになる。
プロシージャは関数や無名ブロック(DO ...)の中で実行できない。CALLコマンドでだけ呼び出せるよ。
トランザクション管理付きプロシージャの例
-- 段階的にコミットするプロシージャ(autocommitモードの接続でのみ動く)
CREATE PROCEDURE process_batch_orders()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT order_id, order_total FROM incoming_orders LOOP
BEGIN
-- 各データバッチを個別に保存
INSERT INTO orders (order_id, total) VALUES (rec.order_id, rec.order_total);
EXCEPTION WHEN OTHERS THEN
INSERT INTO order_errors(order_id, err_text) VALUES (rec.order_id, SQLERRM);
END;
COMMIT;
END LOOP;
END;
$$;
-- プロシージャの呼び出し
CALL process_batch_orders();
毎回COMMITした後、自動的に新しいトランザクションが始まるよ。
PL/pgSQLでの部分的なロールバック(savepoint的な動作)
PL/pgSQL(関数でもプロシージャでも)ではROLLBACK TO SAVEPOINTコマンドはサポートされてない。
コードの一部だけ変更をロールバックしたい場合は、BEGIN ... EXCEPTION ... ENDブロックだけが使える:
BEGIN
-- なんかの処理
BEGIN
-- エラーが起きそうな操作
EXCEPTION WHEN OTHERS THEN
-- このブロック内の変更は全部ロールバックされる
RAISE NOTICE 'ブロック内でロールバックしたよ!';
END;
END;
プロシージャでもSAVEPOINTやRELEASE SAVEPOINTは使えるけど、ROLLBACK TO SAVEPOINTはダメ。段階を分けるために使うけど、実際の制御は例外処理でやるしかない。
制限事項とベストプラクティス
- 関数はアトミックな操作だけ:全部やるか、全部やらないか。何か失敗したら全部ロールバック。
- プロシージャはCALLだけで:しかもSQLコマンド単体で。SELECTや関数からは呼べない。ネストしたトランザクション管理もできるけど、PL/pgSQLの制限はちゃんと守ってね。
- 部分的なロールバックはEXCEPTIONだけ:部分的なロールバック(SAVEPOINT的なやつ)は公式に推奨されてるし、サポートされてるのはこの方法だけ。
- ネストしたプロシージャがトランザクションを管理できるのはCALLで呼ばれたときだけ:それ以外だとエラーになるよ。
ロジックとトランザクションの関係に関する質問
関数の中で「ネストした」トランザクションってできる?
できない。全部ひとつのトランザクションで動く。部分的なロールバックはEXCEPTIONブロックだけ。
関数や無名ブロックの中でCOMMIT/ROLLBACKできる?
できない、それはシンタックスエラー。プロシージャを使ってね。
関数からプロシージャを呼べる?
無理、CALLコマンドだけ。関数やSELECTからは呼べない。
プロシージャの中でROLLBACK TO SAVEPOINTできる?
ダメ!PL/pgSQLでは禁止されてる。EXCEPTIONブロックを使ってね。
GO TO FULL VERSION