CodeGym /课程 /SQL SELF /在触发器中处理错误和事务:EXCEPTION,RAISE

在触发器中处理错误和事务:EXCEPTION,RAISE

SQL SELF
第 58 级 , 课程 3
可用

有时候触发器的行为很迷,这可能是因为:

  • 触发器绑定的函数里有逻辑bug。
  • 数据库约束被破坏(比如唯一性冲突或者数据类型不对)。
  • 事务有问题,比如触发器因为报错导致回滚。
  • 递归调用,比如触发器自己又触发了自己(经常是无意的)。

为了避免这些坑,PostgreSQL允许你在触发器和它的函数里处理错误。你可以用EXCEPTION块和RAISE语句,下面我们就用例子来讲讲怎么用。

EXCEPTION块处理错误

EXCEPTION块让你可以捕获错误,然后写点代码来处理它。这和Python、Java里的try-catch差不多。

在PL/pgSQL函数里,EXCEPTION块的用法是这样的:

BEGIN
    -- 函数的主代码
EXCEPTION
    WHEN <错误类型> THEN
        -- 错误处理代码
END;

这里的<错误类型>就是你想处理的具体错误或者错误组(比如unique_violationdivision_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的消息类型:

  1. DEBUG — 调试信息。
  2. NOTICE — 普通提示信息。
  3. WARNING — 警告。
  4. EXCEPTION — 错误信息,会让函数直接挂掉。

RAISE的语法:

RAISE <消息类型> '消息内容';

你也可以把变量的值带进去:

RAISE NOTICE 'NEW.id的值 = %', NEW.id;

例子:在触发器里调试变量值

假如我们在更新students表时遇到bug,想看看NEWOLD的值到底是什么。可以用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();

现在每次更新记录时,你都能看到OLDNEW的值,如果有错还能看到清楚的错误提示。

触发器里的事务

触发器是在事务上下文里跑的。也就是说,只要触发器或它的函数里有错,整个事务都会回滚。这能优雅地保护数据库不被部分更改。

不过有时候这种机制也会带来麻烦:

  • 如果触发器里的错误只是数据不对,有时候你只想回滚一部分操作。
  • 要记住,回滚事务会把触发器和触发它的整个操作都撤销。

例子:在触发器里用事务

举个例子,假设我们要做点业务逻辑,包括两步:更新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

性能下降。 复杂的触发器会拖慢INSERTUPDATEDELETE。解决办法:让触发器逻辑尽量简单,别写太重的查询。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION