有时候触发器的行为很迷,这可能是因为:
- 触发器绑定的函数里有逻辑bug。
- 数据库约束被破坏(比如唯一性冲突或者数据类型不对)。
- 事务有问题,比如触发器因为报错导致回滚。
- 递归调用,比如触发器自己又触发了自己(经常是无意的)。
为了避免这些坑,PostgreSQL允许你在触发器和它的函数里处理错误。你可以用EXCEPTION块和RAISE语句,下面我们就用例子来讲讲怎么用。
用EXCEPTION块处理错误
EXCEPTION块让你可以捕获错误,然后写点代码来处理它。这和Python、Java里的try-catch差不多。
在PL/pgSQL函数里,EXCEPTION块的用法是这样的:
BEGIN
-- 函数的主代码
EXCEPTION
WHEN <错误类型> THEN
-- 错误处理代码
END;
这里的<错误类型>就是你想处理的具体错误或者错误组(比如unique_violation、division_by_zero等等)。
例子:在触发器里记录错误日志
假设我们有个logs表,想把往students表插入数据时发生的错误都记下来。比如:
先建个日志表
CREATE TABLE logs (
id SERIAL PRIMARY KEY,
error_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
error_message TEXT
);
再写个带错误处理的函数
CREATE OR REPLACE FUNCTION track_insert_errors()
RETURNS TRIGGER AS $$
BEGIN
-- 试着执行主代码
BEGIN
-- 举个“出错”的例子:除以0
PERFORM 1 / (NEW.some_value - NEW.some_value);
EXCEPTION
WHEN division_by_zero THEN
-- 如果发生了除以0的错误,把它写进日志
INSERT INTO logs (error_message) VALUES ('插入students时出现除以0的错误');
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
再建个触发器
CREATE TRIGGER before_insert_students
BEFORE INSERT ON students
FOR EACH ROW
EXECUTE FUNCTION track_insert_errors();
现在,如果往students表插数据时遇到除以零的错误,这个错误会被处理掉,相关信息会写进logs表。
用RAISE做诊断和调试
RAISE语句可以输出警告、错误或者调试信息。调试触发器的时候,这玩意儿特别有用,能帮你搞清楚到底哪里出问题了。
RAISE的消息类型:
DEBUG— 调试信息。NOTICE— 普通提示信息。WARNING— 警告。EXCEPTION— 错误信息,会让函数直接挂掉。
RAISE的语法:
RAISE <消息类型> '消息内容';
你也可以把变量的值带进去:
RAISE NOTICE 'NEW.id的值 = %', NEW.id;
例子:在触发器里调试变量值
假如我们在更新students表时遇到bug,想看看NEW和OLD的值到底是什么。可以用RAISE:
CREATE OR REPLACE FUNCTION debug_student_update()
RETURNS TRIGGER AS $$
BEGIN
RAISE NOTICE 'OLD.id = %, NEW.id = %', OLD.id, NEW.id;
-- 举个会报错的条件:
IF NEW.some_field IS NULL THEN
RAISE EXCEPTION 'some_field字段不能为NULL';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER after_update_students
AFTER UPDATE ON students
FOR EACH ROW
EXECUTE FUNCTION debug_student_update();
现在每次更新记录时,你都能看到OLD和NEW的值,如果有错还能看到清楚的错误提示。
触发器里的事务
触发器是在事务上下文里跑的。也就是说,只要触发器或它的函数里有错,整个事务都会回滚。这能优雅地保护数据库不被部分更改。
不过有时候这种机制也会带来麻烦:
- 如果触发器里的错误只是数据不对,有时候你只想回滚一部分操作。
- 要记住,回滚事务会把触发器和触发它的整个操作都撤销。
例子:在触发器里用事务
举个例子,假设我们要做点业务逻辑,包括两步:更新students表和往logs写日志。如果其中一步失败,整个事务都会回滚。
CREATE OR REPLACE FUNCTION transactional_student_update()
RETURNS TRIGGER AS $$
BEGIN
-- 记录更新尝试
INSERT INTO logs (error_message) VALUES ('尝试更新id为 ' || NEW.id || ' 的学生');
-- 检查业务条件
IF NEW.some_value IS NULL THEN
RAISE EXCEPTION 'some_value字段不能为NULL';
END IF;
-- 如果一切正常,返回NEW
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER before_update_students
BEFORE UPDATE ON students
FOR EACH ROW
EXECUTE FUNCTION transactional_student_update();
用触发器时常见的坑和怎么避免
开发时经常遇到的错误:
递归触发器。 就是触发器改了数据又把自己触发了。解决办法:用WHEN条件或者加个flag防止重复触发。
因为错误导致整个事务回滚。 有时候你不想这样,尤其是触发器和主数据没啥直接关系时。解决办法:合理用EXCEPTION块。
调试信息太多。 日志一堆垃圾,分析起来很难。解决办法:只在开发和测试时用RAISE。
性能下降。 复杂的触发器会拖慢INSERT、UPDATE或DELETE。解决办法:让触发器逻辑尽量简单,别写太重的查询。
GO TO FULL VERSION