【问题标题】:Raise error without rollback in plpgsql/postgresql在 plpgsql/postgresql 中引发错误而不回滚
【发布时间】:2020-04-01 11:37:19
【问题描述】:

我有两个存储函数。 delete_item 删除一项,在actionlog 表中记录它的成功或失败,它在错误时返回1,在成功运行时返回0

其次,我有另一个函数remove_expired,它可以找到要删除的内容并循环调用delete_item

所有这一切都旨在使用简单的 bash 脚本调用(操作的硬性要求,因此不讨论这样的调用),并且当事情不适用于他们的报告工具时,它必须给出错误代码.

我们希望所有可能的删除都成功(我们不期望错误,但人仍然是人,错误确实会发生),所以如果我们要删除 10 个项目并且 1 个失败,我们仍然希望其他 9 个被删除。

其次,我们真的希望日志在成功和错误情况下都在表actionlog 中。即,我们希望该日志是完整的。

由于 plpgsql 函数不允许手动事务管理,这似乎不是一种选择(除非我错过了规避此问题的方法?)。

到目前为止,我发现实现这一点的唯一方法是将脚本包装在 plpgsql 之外,但我们非常希望这可以在纯 plpgsql 中实现,因此我们可以给操作一个 pssql -C ... 命令,然后他们不应该关心其他任何事情。

重现问题的SQL:

DROP FUNCTION IF EXISTS remove_expired(timestamp with time zone);
DROP FUNCTION IF EXISTS delete_item(integer);

DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;

CREATE TABLE test (
    id         serial primary key       not null,
    t          timestamp with time zone not null
);

CREATE TABLE evil (
    test_id integer not null references test(id)
);

CREATE TABLE actionlog (
    eventTime timestamp with time zone not null default now(),
    message   text                     not null
);


INSERT INTO test (actualTime, t)
VALUES ('2020-04-01T10:00:00+0200'),
       ('2020-04-01T10:15:00+0200'), -- Will not be deleable due to foreign key
       ('2020-04-01T10:30:00+0200')
;

INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;


CREATE OR REPLACE FUNCTION remove_expired(timestamp with time zone)
    RETURNS void
AS
$$
DECLARE
    test_id int;
    failure_count int = 0;
BEGIN
    FOR test_id IN
        SELECT id FROM test WHERE t < $1
    LOOP
        failure_count := delete_item(test_id) + failure_count;
    END LOOP;

    IF failure_count > 0 THEN
        -- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
        RAISE 'There was one or more errors deleting. See the log for details';
    END IF;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION delete_item(integer)
    RETURNS integer
AS
$$
BEGIN
    DELETE FROM test WHERE id = $1;
    INSERT INTO actionlog (message)
        VALUES ('Deleted with ID: ' || $1);
    RETURN 0;
EXCEPTION WHEN OTHERS THEN
    INSERT INTO actionlog (message)
        VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
    RETURN 1;
END
$$ LANGUAGE plpgsql;

提前感谢您提供任何有用的意见

【问题讨论】:

  • 如果你有 Postgres 版本>=11,你应该考虑让 delete_item 成为一个过程而不是函数。它们允许您提交和回滚,从而将错误提交到数据库。 0/1 可以返回 in/inout 参数。函数总是会在出错时回滚所有内容。个人在程序方面不是很老练,但我很确定这是要走的路。 postgresql.org/docs/11/sql-createprocedure.html

标签: postgresql plpgsql


【解决方案1】:

您可以在 PostgreSQL 11 或 PostgreSQL 12 中获得接近您期望的东西,但只能使用过程,因为如前所述,函数总是会在出现错误时回滚所有内容。

与:

DROP PROCEDURE IF EXISTS remove_expired(timestamp with time zone);
DROP PROCEDURE IF EXISTS delete_item(integer);
DROP FUNCTION  IF EXISTS f_removed_expired;
DROP SEQUENCE  IF EXISTS failure_count_seq;

DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;

CREATE TABLE test (
    id         serial primary key       not null,
    t          timestamp with time zone not null
);

CREATE TABLE evil (
    test_id integer not null references test(id)
);

CREATE TABLE actionlog (
    eventTime timestamp with time zone not null default now(),
    message   text                     not null
);


INSERT INTO test (t)
VALUES ('2020-04-01T10:00:00+0200'),
       ('2020-04-01T10:15:00+0200'), -- Will not be removed due to foreign key
       ('2020-04-01T10:30:00+0200')
;

select * from test where t < current_timestamp;

INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;

CREATE SEQUENCE failure_count_seq MINVALUE 0;
SELECT SETVAL('failure_count_seq', 0, true);

CREATE OR REPLACE PROCEDURE remove_expired(timestamp with time zone)
AS
$$
DECLARE
    test_id int;
    failure_count int = 0;
    return_code int;
BEGIN
    FOR test_id IN
        SELECT id FROM test WHERE t < $1
    LOOP
        call delete_item(test_id);
        COMMIT;
    END LOOP;

    SELECT currval('failure_count_seq') INTO failure_count; 
    IF failure_count > 0 THEN
        -- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
        RAISE 'There was one or more errors deleting. See the log for details';
    END IF;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE PROCEDURE delete_item(in integer)
AS
$$
DECLARE 
 forget_value int;
BEGIN
    DELETE FROM test WHERE id = $1;
    INSERT INTO actionlog (message)
        VALUES ('Deleted with ID: ' || $1);
EXCEPTION WHEN OTHERS THEN
    INSERT INTO actionlog (message)
        VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
    COMMIT;
    SELECT NEXTVAL('failure_count_seq') INTO forget_value;
END
$$ LANGUAGE plpgsql;
--

我明白了:

select * from test;
 id |           t            
----+------------------------
  1 | 2020-04-01 10:00:00+02
  2 | 2020-04-01 10:15:00+02
  3 | 2020-04-01 10:30:00+02
(3 rows)

select current_timestamp;
       current_timestamp       
-------------------------------
 2020-04-01 16:52:26.171975+02
(1 row)

call remove_expired(current_timestamp);
psql:test.sql:80: ERROR:  There was one or more errors deleting. See the log for details
CONTEXT:  PL/pgSQL function remove_expired(timestamp with time zone) line 17 at RAISE
select currval('failure_count_seq');
 currval 
---------
       1
(1 row)

select * from test;
 id |           t            
----+------------------------
  2 | 2020-04-01 10:15:00+02
(1 row)

select * from actionlog;
           eventtime           |                                                                  message                                                                  
-------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------
 2020-04-01 16:52:26.172173+02 | Deleted with ID: 1
 2020-04-01 16:52:26.179794+02 | Error deleting ID: 2. The error was: update or delete on table "test" violates foreign key constraint "evil_test_id_fkey" on table "evil"
 2020-04-01 16:52:26.196503+02 | Deleted with ID: 3
(3 rows)

我使用一个序列来记录失败的次数:你可以使用这个序列来测试失败并返回正确的返回码。

【讨论】:

  • 会尽快测试。不幸的是,我们可能无法超过 10,但如果它在 11 中有效,我仍然对您的回答非常满意,非常感谢 :)
  • 按预期工作,非常感谢。对于阅读本文的人:@pifor 答案中的序列原因很可能是PROCEDURE 无法返回任何内容。同样可以通过使用FUNCTIONPROCEDURE 的组合、临时表等来实现。不确定我更喜欢哪个,所有这些都感觉有点不一致,作为我的解决方法:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-11
  • 1970-01-01
  • 1970-01-01
  • 2012-04-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多