CodeGym /课程 /SQL SELF /错误处理和回到初始状态:EXCEPTION,RAISE

错误处理和回到初始状态:EXCEPTION,RAISE

SQL SELF
第 53 级, 课程 3
可用

在PostgreSQL里,错误可能因为很多原因冒出来:比如约束被破坏(NOT NULL、UNIQUE、CHECK),语法写错,值重复啥的。如果你不去捕获和处理这些错误,整个外部事务可能直接全被回滚。想让业务更稳,正确处理这些错误很重要。

PL/pgSQL给你了一个很强大的BEGIN ... EXCEPTION ... END块机制,可以在函数和过程里捕获和处理错误。这玩意儿有点像Python或者Java里的try-catch,不过在PostgreSQL事务里有点自己的玩法。

重点:

每个BEGIN ... EXCEPTION ... END块都像是个“虚拟savepoint”。如果出错了——这个块里的所有改动会自动回滚。这是唯一正确的部分回滚方式,在PL/pgSQL的函数和过程里。

用EXCEPTION处理错误的语法

BEGIN
    -- 主要代码
EXCEPTION
    WHEN 错误类型 THEN
        -- 处理具体错误
    WHEN 另一个错误类型 THEN
        -- 另一个处理器
    WHEN OTHERS THEN
        -- 处理所有其他错误
END;

在函数/过程里的例子

DO $$
BEGIN
    RAISE NOTICE '现在要出错了...';
    PERFORM 1 / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE '捕获到除以零啦!';
END;
$$;

例子:带错误处理和回滚的更新

假设有个订单表:

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    amount NUMERIC NOT NULL,
    status TEXT NOT NULL
);

来写个函数,更新订单状态,出错就啥都不改:

CREATE OR REPLACE FUNCTION update_order_status(order_id INT, new_status TEXT)
RETURNS VOID AS $$
BEGIN
    BEGIN
        UPDATE orders
        SET status = new_status
        WHERE id = order_id;

        -- 模拟错误
        IF new_status = 'FAIL' THEN
            RAISE EXCEPTION '模拟一下错误!';
        END IF;

        RAISE NOTICE '订单状态 % 已更新', order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE '更新订单 % 时出错: %', order_id, SQLERRM;
            -- 这个块里的所有改动会自动回滚!
            -- 如果需要,可以把错误再抛出去给外面处理
            RAISE;
    END;
END;
$$ LANGUAGE plpgsql;

在带显式事务管理的过程里怎么用

在过程(CREATE PROCEDURE)里你可以用COMMIT、ROLLBACK、SAVEPOINT,但不能用ROLLBACK TO SAVEPOINT。如果你只想回滚过程里一部分操作,还是得用BEGIN ... EXCEPTION ... END:

CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    -- 整个过程可以用COMMIT/ROLLBACK,但要回滚某一步:
    BEGIN
        UPDATE accounts
        SET balance = balance - amount
        WHERE id = (SELECT account_id FROM orders WHERE id = order_id);

        -- 错误
        IF amount < 0 THEN
            RAISE EXCEPTION '金额不能为负数!';
        END IF;

        UPDATE orders SET status = 'PAID' WHERE id = order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE '处理订单 % 支付时出错: %', order_id, SQLERRM;
            -- 这个块里的改动会自动回滚
    END;

    COMMIT; -- 只有在过程里才能显式提交事务!
END;
$$;

错误日志记录

不仅要捕获错误,还要把它们存下来,方便以后分析。

CREATE TABLE error_log (
    id SERIAL PRIMARY KEY,
    order_id INT,
    error_message TEXT,
    error_time TIMESTAMP DEFAULT now()
);

在函数或过程里:

EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO error_log (order_id, error_message)
        VALUES (order_id, SQLERRM);
        RAISE NOTICE '错误已记录到日志: %', SQLERRM;
        RAISE;

PostgreSQL 17里的重要限制和细节

在函数里(CREATE FUNCTION ... )不能用事务管理命令(BEGIN、COMMIT、ROLLBACK、SAVEPOINT、ROLLBACK TO SAVEPOINT、RELEASE SAVEPOINT)! 所有函数都是整体在外部事务里跑的。

在过程里(CREATE PROCEDURE ... )可以显式写SAVEPOINT、RELEASE SAVEPOINT、COMMIT、ROLLBACK。但是:ROLLBACK TO SAVEPOINT——禁止!(你要是在PL/pgSQL过程里用ROLLBACK TO SAVEPOINT会直接报语法错。)

在函数和过程里“部分回滚”只能靠BEGIN ... EXCEPTION ... END块。 一旦出错,这个块里的东西自动回滚,后面还能继续执行。

过程(CREATE PROCEDURE)不能在函数里或者用SELECT调用——只能用CALL ...单独跑。

PL/pgSQL里怎么实现“部分回滚”?

唯一靠谱的办法——用BEGIN ... EXCEPTION ... END块来处理错误。这个块会自动建个保存点,出错时只回滚块里的内容,不影响过程/函数的其他部分。

EXCEPTION例子(推荐做法):

CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
    -- 随便写点代码
    BEGIN
        -- 这里出错不会回滚整个过程,
        -- 只回滚这个块!
        INSERT INTO demo VALUES ('bad data'); -- 可能会出错
    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE '错误已处理,块内改动已撤销';
    END;
    -- 这里还能继续执行!
END;
$$;

例子:批量导入时防止整个过程回滚

CREATE OR REPLACE PROCEDURE load_big_batch()
LANGUAGE plpgsql
AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT * FROM import_table LOOP
        BEGIN
            INSERT INTO target_table (col1, col2)
            VALUES (rec.col1, rec.col2);
        EXCEPTION WHEN OTHERS THEN
            INSERT INTO import_errors (err_msg)
            VALUES ('记录出错: ' || rec.col1 || ': ' || SQLERRM);
            -- 这个块里的改动已撤销!
        END;
    END LOOP;
    COMMIT;  -- 只有过程在外部没有事务时才能用!
END;
$$;

-- 调用过程
CALL load_big_batch();

注意:如果你在客户端已经开了事务(比如Python里autocommit=False),那在过程里用COMMIT或SAVEPOINT会报错。

嵌套保存点和EXCEPTION的使用建议

  1. 别在PL/pgSQL里用ROLLBACK TO SAVEPOINT! 会直接语法错。
  2. 想部分回滚就用嵌套的BEGIN ... EXCEPTION ... END块。
  3. 记住,过程里的COMMIT和ROLLBACK会重启事务——只有在autocommit模式下跑过程时才能用!
  4. 把错误日志单独存表里,别丢了有问题的数据。
  5. 如果业务操作必须原子(要么全成要么全失败)——用函数,别在里面写COMMIT/ROLLBACK;如果要分步处理——用过程。

例子:批量导入+部分处理

CREATE OR REPLACE PROCEDURE import_batch()
LANGUAGE plpgsql
AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT * FROM staging_table LOOP
        BEGIN
            INSERT INTO data_table (data)
            VALUES (rec.data);
        EXCEPTION
            WHEN unique_violation THEN
                INSERT INTO import_log (msg)
                VALUES ('重复: ' || rec.data);
            WHEN OTHERS THEN
                INSERT INTO import_log (msg)
                VALUES ('出错: ' || rec.data || ' — ' || SQLERRM);
        END;
    END LOOP;
END;
$$;

PostgreSQL 17你必须记住的点:

在PL/pgSQL的过程里允许用SAVEPOINT、RELEASE SAVEPOINT、COMMIT、ROLLBACK,但ROLLBACK TO SAVEPOINT——不行。

函数和过程里的“部分回滚”只能靠嵌套BEGIN ... EXCEPTION ... END块。

事务最好在外部控制(比如连接参数和autocommit),过程内部就用上面说的错误处理机制。

2
任务
SQL SELF, 第 53 级, 课程 3
已锁定
使用 EXCEPTION 和回滚
使用 EXCEPTION 和回滚
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION