即使函数中止,也会在 UDF 中持久插入

art*_*hur 5 postgresql insert transaction plpgsql postgresql-9.4

我有一个相当复杂的 UDF(在一堆表中移动并创建一堆新表),其中可能会发生多次中止。在每次操作之前,我想记录操作发生的时间和查询本身。UDF 如下所示:

log function_start

log sql1
execute sql1

log sql2
execute sql2

...

log sqlN
execute sqlN

log function_end
Run Code Online (Sandbox Code Playgroud)

每条日志语句都意味着向下表中插入一条新记录:

CREATE TABLE backup_logs
(
  id serial NOT NULL,
  t timestamp with time zone default now(),
  sql text,
  CONSTRAINT backup_logs_pkey PRIMARY KEY (id)
)
Run Code Online (Sandbox Code Playgroud)

如果发生中止,我希望sql1, sql2, ... sqlN回滚,但inserto into backup_logs会继续存在。问题:我怎样才能实现这一目标?

Erw*_*ter 4

当异常发生时,一切都会回滚。您想要在当前事务上下文“外部”执行选定的代码,这通常称为自治事务。目前尚未实现(从第 9.4 页开始)。Postgres TODO wiki 中有一个项目,但这是一个棘手的问题,不要屏住呼吸。

更新: Postgres 11 或更高版本允许COMMITin 过程。(但是提交一些更改而不是其他更改仍然很棘手。)请参阅:

现在您可以使用附加模块dblink。它提供了在单独的连接(因此也是单独的事务)中连接到另一个数据库以在那里执行 SQL 的功能。事情已经过去了,无法挽回。通过再次连接到同一个数据库,您可以有效地实现自治事务(需要一些额外的开销)。速度相当快。

概念证明

准备工作

您需要为每个数据库(运行该函数的位置)安装一次 dblink:

CREATE EXTENSION dblink;
Run Code Online (Sandbox Code Playgroud)

为了方便起见,我将连接信息封装在FOREIGN SERVERplus中USER MAPPING

CREATE SERVER myserver FOREIGN DATA WRAPPER dblink_fdw
OPTIONS (hostaddr '127.0.0.1', dbname 'test');

CREATE USER MAPPING FOR role_source SERVER myserver
OPTIONS (user 'role_target', password 'secret');
Run Code Online (Sandbox Code Playgroud)

连接字符串有多种选项。该示例针对名为localhost 上的数据库的用户role_source进行连接。源和目标可以是相同的角色名称。我在这里使用密码(简单的示例),但我更喜欢无密码访问,因此我们不必保存密码(并且它不会最终出现在备份等中):role_targettest127.0.0.1

但是,根据文档

只有超级用户可以用来dblink_connect创建非密码验证的连接。如果非超级用户需要此功能,请dblink_connect_u改用。

或者创建一个SECURITY DEFINER函数来封装相关部分。见下文。

主功能

CREATE OR REPLACE FUNCTION f_work()
  RETURNS void
  LANGUAGE plpgsql VOLATILE AS
$func$
DECLARE
   sql1 text := 'SELECT 1';  -- dummy code
   sql2 text := 'SELECT 2';

BEGIN
   PERFORM dblink_connect('myserver');  -- name of foreign server
   PERFORM dblink_exec($f$INSERT INTO backup_logs(sql)
                          VALUES ('f_work() start')$f$);   -- log start

   PERFORM dblink_exec('INSERT INTO backup_logs(sql)
                        SELECT ' || quote_literal(sql1));  -- log sql1
   EXECUTE sql1;

   -- RAISE EXCEPTION 'foo'; -- unquote to test error case

   PERFORM dblink_exec('INSERT INTO backup_logs(sql)
                        SELECT ' || quote_literal(sql2));  -- log sql2
   EXECUTE sql2;


   PERFORM dblink_exec($f$INSERT INTO backup_logs(sql)
                          VALUES ('f_work() end')$f$);     -- log end
   PERFORM dblink_disconnect();
END
$func$;
Run Code Online (Sandbox Code Playgroud)

请确保USER MAPPING您正在运行此函数的角色有一个 ,或者使其成为SECURITY DEFINER您要使用的角色所拥有的函数: