在 Postgres 触发函数中调用异常之前执行操作

Posted

技术标签:

【中文标题】在 Postgres 触发函数中调用异常之前执行操作【英文标题】:Perform action before exception call in Postgres trigger function 【发布时间】:2014-09-03 17:36:49 【问题描述】:

这里是 Postgres 8.4。想象一下this code snippet from Postgres doc:

CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
    -- Check that empname and salary are given
    IF NEW.empname IS NULL THEN
        RAISE EXCEPTION 'empname cannot be null';
    END IF;
    IF NEW.salary IS NULL THEN
        RAISE EXCEPTION '% cannot have null salary', NEW.empname;
    END IF;

    -- Who works for us when she must pay for it?
    IF NEW.salary < 0 THEN
        RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
    END IF;

    -- Remember who changed the payroll when
    NEW.last_date := current_timestamp;
    NEW.last_user := current_user;
    RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;

如果我们想做一些事情,比如登录自定义表,这些异常:

-- Check that empname and salary are given
IF NEW.empname IS NULL THEN
    INSERT INTO my_log_table ('User didn't supplied empname')
    RAISE EXCEPTION 'empname cannot be null';
END IF;

它不会起作用,因为我们在 RAISE EXCEPTION 调用之前放置的任何内容都被 RAISE EXCEPTION 回滚所撤消,即我们创建的 my_log_table 行将在调用 RAISE EXCEPTION 时立即被删除。

完成这样的事情的最佳方法是什么?也许捕捉到我们的自定义异常?

关闭回滚@TRIGGER 不是一个选项,我需要它。

【问题讨论】:

你真正想要的是一个子事务(大致相当于 Oracle 的 pragma autonomous。不幸的是,这仍然只是一个提议,还没有实现(Postgres 9.3)。你可以检查不过,这个post 详细说明了一个常见的解决方法。 这可能行得通,虽然我感觉在这种特殊情况下就像用大锤敲碎坚果:) 当您从任何来源添加长引用时,请同时添加指向该来源的链接。我添加了 Postgres 8.4 手册的链接。 【参考方案1】:

你可以trap errors/捕获异常。

EXCEPTION 块中,您可以执行任何其他操作,例如插入到另一个表中。之后您可以重新引发异常以传播出去,但这会将包括 INSERT 在内的整个事务回滚到日志表(除非异常被包装并在外部函数中捕获)。

可以

使用诸如 dblink 调用之类的技巧来模拟自治事务,当包装事务回滚时,该事务不会撤消。相关:

How to use (install) dblink in PostgreSQL? dblink can't update a table on the same database in an after UPDATE trigger Does Postgres support nested or autonomous transactions?

RAISE NOTICEWARNING 另外,ROOLBACK 也无法撤消。

RAISE 一个不同的EXCEPTION 与您自己的文字。

或者,您可以只取消触发触发函数的行引发异常。交易中的其他一切都正常进行。

假设这是一个触发器ON UPDATE,并且您有另一个具有相同结构的表可以将失败的 INSERT 写入:

CREATE OR REPLACE FUNCTION emp_stamp()
  RETURNS trigger AS
$func$
BEGIN
    -- Check that empname and salary are given
    IF NEW.empname IS NULL THEN
         RAISE EXCEPTION 'empname cannot be null';
    END IF;

    IF ...

    RETURN NEW;    -- regular end

EXCEPTION WHEN others THEN  -- or be more specific
    INSERT INTO log_tbl VALUES (NEW.*); -- identical table structure
    RETURN NULL;   -- cancel row
END
$func$ LANGUAGE plpgsql;

请注意,NEW 包含异常发生前行的状态,包括同一函数中先前成功的语句。

触发器:

CREATE TRIGGER emp_stamp
BEFORE INSERT OR UPDATE ON tbl
FOR EACH ROW EXECUTE PROCEDURE emp_stamp();

【讨论】:

我最终会使用这样的东西,我丢失了在应用程序级别捕获并显示的异常,但至少触发触发器的 plpgsql 函数可以向应用程序返回错误值(因为它检测到没有更新的行-FOUND-) 我发现其他更复杂的解决方案不值得麻烦【参考方案2】:

实际上你有两个选择。

    您可以通过在 raise 上使用某个级别来登录到 postgresql 日志检查manual 在您的应用程序中记录错误,而不是在 Db 事务中。例如,不应该像这样在触发器函数中检查负工资。或者应该在插入调用时处理异常。

【讨论】:

1.实际上,我要保留的不是 log_add(这只是一个示例),而是不同的东西(插入另一个表)。 2.我知道这一点,但是在应用程序级别执行此操作会更慢,因为我必须使用更多数据库查询来检查条件。我只是想知道 postgresql 是否有办法在 TRIGGERS 中做到这一点

以上是关于在 Postgres 触发函数中调用异常之前执行操作的主要内容,如果未能解决你的问题,请参考以下文章

从java调用带有UDT参数的postgres函数给SQL尚未实现异常

在 postgres 9.4 中使用触发器执行外部程序

PostMessage 不会触发消息处理程序

这个 postgres 触发函数有啥问题?

原始函数之前的 then 子句中的角度触发函数

AWS Glue - 在插入之前截断目标 postgres 表