在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的使用建议
- 别在PL/pgSQL里用ROLLBACK TO SAVEPOINT! 会直接语法错。
- 想部分回滚就用嵌套的BEGIN ... EXCEPTION ... END块。
- 记住,过程里的COMMIT和ROLLBACK会重启事务——只有在autocommit模式下跑过程时才能用!
- 把错误日志单独存表里,别丢了有问题的数据。
- 如果业务操作必须原子(要么全成要么全失败)——用函数,别在里面写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),过程内部就用上面说的错误处理机制。
GO TO FULL VERSION