CodeGym /课程 /SQL SELF /在事务中调用procedure和function

在事务中调用procedure和function

SQL SELF
第 53 级, 课程 1
可用

现在的数据库系统,业务逻辑经常写在服务端——用procedure和function来搞。用PostgreSQL的时候,得分清楚function和procedure的区别(特别是11+版本加了procedure之后),还有它们跟事务是怎么配合的。

下面我给你讲讲PostgreSQL 17里,事务机制、嵌套调用、procedure/function里怎么部分回滚,都是官方文档和现在的限制。

核心概念:function vs procedure

function(CREATE FUNCTION)——永远都在一个外部事务里跑;function里不能用显式事务命令(BEGIN、COMMIT、ROLLBACK、SAVEPOINT)。

  • 所有改动只会在外部事务级别commit或者rollback。
  • 想在function里“部分回滚”,可以用BEGIN ... EXCEPTION ... END,但不能在function里commit。

procedure(CREATE PROCEDURE)——就是为了让你能在服务端直接控制事务(比如分阶段commit、回滚啥的)。

  • 在procedure(PL/pgSQL)里可以用COMMIT、ROLLBACK、SAVEPOINT、RELEASE SAVEPOINT。
  • 注意:在PL/pgSQL的procedure里不能用ROLLBACK TO SAVEPOINT(会报语法错)。
  • procedure只能用SQL命令CALL ...来调,不能用SELECT或者在别的function里调。

怎么在一个procedure/function里调另一个?

function可以直接像普通函数那样调别的function:

-- 例子:计算折扣的function
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;

-- 订单处理function会调另一个function
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;

所有操作都在同一个外部事务里!任何function里出错,所有改动都会回滚。

procedure调用和嵌套事务

procedure可以在别的procedure里用CALL ...调(PostgreSQL 17支持CALL proc1() -> CALL proc2()这种调用栈),但事务规则还是一样:

  • 事务命令(COMMIT、ROLLBACK、SAVEPOINT、RELEASE SAVEPOINT)只能在最外层procedure里用。
  • 如果procedure里有事务控制,结果你在已经有显式事务的情况下(比如客户端不开autocommit)调它,COMMIT/SAVEPOINT会报错。
注意:

procedure不能在function或者匿名块(DO ...)里调。只能用CALL单独调。

带事务控制的procedure例子

-- 分批commit的procedure(只在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;
$$;

-- 调用procedure
CALL process_batch_orders();

每次COMMIT后会自动开新事务。

PL/pgSQL里的部分回滚(类似savepoint的效果)

PL/pgSQL(无论function还是procedure)不支持ROLLBACK TO SAVEPOINT命令。

想回滚部分代码,只能用BEGIN ... EXCEPTION ... END块:

BEGIN
    -- 一些操作
    BEGIN
        -- 可能出错的操作
    EXCEPTION WHEN OTHERS THEN
        -- 这个块里的改动会回滚
        RAISE NOTICE '块内回滚!';
    END;
END;

在procedure里也可以用SAVEPOINT和RELEASE SAVEPOINT,但不能用ROLLBACK TO SAVEPOINT。它们主要用来分阶段,真正控制还是得靠异常处理。

限制和最佳实践

  1. function——只能做原子操作:要么全成,要么全回滚。出错就全撤。
  2. procedure——只能用CALL调:只能用单独的SQL命令,不能在SELECT/function里调。嵌套事务可以,但得严格按PL/pgSQL的限制来。
  3. 部分回滚——只能用EXCEPTION:官方推荐和支持的部分回滚方式(类似SAVEPOINT)。
  4. 嵌套procedure只有用CALL才能控制事务:否则会报错。

逻辑和事务交互的常见问题

我能在function里搞“嵌套”事务吗?

不能。所有操作都在一个事务里。想部分回滚——只能用EXCEPTION块。

我能在function或匿名块里用COMMIT/ROLLBACK吗?

不能,会报语法错。用procedure吧。

能不能在function里调procedure?

不能,只能用CALL命令调。function/SELECT里不行。

procedure里能不能ROLLBACK TO SAVEPOINT?

不能!PL/pgSQL里不让用。用EXCEPTION块吧。

2
任务
SQL SELF, 第 53 级, 课程 1
已锁定
创建和调用用于计算和记录日志的过程
创建和调用用于计算和记录日志的过程
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION