50

我正在使用 PostgreSQL 9.1。我的数据库是结构化的,因此有我的应用程序使用的实际表。对于每个表,都有一个历史表,它只存储更改历史。历史表包含与实际表和字段相同的字段,这些字段形成了一些额外的信息,例如。编辑时间。历史表仅由触发器处理。

我有两种触发器:

  1. Before INSERT触发器在创建表时向表中添加一些额外信息(例如 create_time)。
  2. Before UPDATE触发器和before DELETE触发器将旧值从实际表复制到历史表。

问题是我想使用触发器来存储进行这些更改的用户的 ID。我所说的 id 是指来自 php 应用程序的 id,而不是 PostgreSQL 用户 id。

有没有合理的方法来做到这一点?

使用 INSERT 和 UPDATE 可以将额外的 id 字段添加到实际表中,并将用户 id 作为 SQL 查询的一部分传递给 SQL。据我所知,这不适用于 DELETE。

所有触发器的结构如下:

CREATE OR REPLACE FUNCTION before_delete_customer() RETURNS trigger AS $BODY$
BEGIN
    INSERT INTO _customer (
        edited_by,
        edit_time,
        field1,
        field2,
        ...,
        fieldN
    ) VALUES (
        -1, // <- This should be user id.
        NOW(),
        OLD.field1,
        OLD.field2,
        ...,
        OLD.fieldN
    );
    RETURN OLD;
END; $BODY$
LANGUAGE plpgsql
4

4 回答 4

54

选项包括:

  • 当您打开连接时,CREATE TEMPORARY TABLE current_app_user(username text); INSERT INTO current_app_user(username) VALUES ('the_user');. 然后在您的触发器中,SELECT username FROM current_app_user获取当前用户名,可能作为子查询。

  • 在为自定义 GUCpostgresql.conf创建条目时,例如. 每当您创建连接时运行. 然后在触发器中,使用函数获取值。实际上,您正在滥用 GUC 机制来提供会话变量。阅读适合您的服务器版本的文档,因为自定义 GUC 在 9.2 中发生了更改my_app.username = 'unknown';SET my_app.username = 'the_user';current_setting('my_app.username')

  • 调整您的应用程序,使其具有适用于每个应用程序用户的数据库角色。SET ROLE在工作之前给那个用户。这不仅可以让您使用内置的类current_user变量函数SELECT current_user;,还可以让您在数据库中强制执行安全性。看到这个问题。您可以直接以用户身份登录,而不是使用SET ROLE,但这往往会使连接池变得困难。

在这三种情况下,您都在使用连接池,当您将连接返回到池时必须小心DISCARD ALL;。(虽然它没有记录为这样做DISCARD ALL但确实如此RESET ROLE)。

演示的常见设置:

CREATE TABLE tg_demo(blah text);
INSERT INTO tg_demo(blah) VALUES ('spam'),('eggs');

-- Placeholder; will be replaced by demo functions
CREATE OR REPLACE FUNCTION get_app_user() RETURNS text AS $$
SELECT 'unknown';
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION tg_demo_trigger() RETURNS trigger AS $$
BEGIN
    RAISE NOTICE 'Current user is: %',get_app_user();
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tg_demo_tg
AFTER INSERT OR UPDATE OR DELETE ON tg_demo 
FOR EACH ROW EXECUTE PROCEDURE tg_demo_trigger();

使用 GUC:

  • 在 的CUSTOMIZED OPTIONS部分中postgresql.conf,添加一行,如myapp.username = 'unknown_user'。在 9.2 之前的 PostgreSQL 版本上,您还必须设置custom_variable_classes = 'myapp'.
  • 重新启动 PostgreSQL。您现在将能够SHOW myapp.username获得价值unknown_user

现在您可以SET myapp.username = 'the_user';在建立连接时使用,或者如果您希望它是事务本地的,则可以在事务SET LOCAL myapp.username = 'the_user';之后使用,这对于池连接很方便。BEGIN

get_app_user函数定义:

CREATE OR REPLACE FUNCTION get_app_user() RETURNS text AS $$
    SELECT current_setting('myapp.username');
$$ LANGUAGE sql;

SET LOCAL使用交易本地当前用户名的演示:

regress=> BEGIN;
BEGIN
regress=> SET LOCAL myapp.username = 'test_user';
SET
regress=> INSERT INTO tg_demo(blah) VALUES ('42');
NOTICE:  Current user is: test_user
INSERT 0 1
regress=> COMMIT;
COMMIT
regress=> SHOW myapp.username;
 myapp.username 
----------------
 unknown_user
(1 row)

如果您使用SET而不是SET LOCAL设置,则不会在提交/回滚时恢复,因此它在整个会话中是持久的。它仍然被重置DISCARD ALL

regress=> SET myapp.username = 'test';
SET
regress=> SHOW myapp.username;
 myapp.username 
----------------
 test
(1 row)

regress=> DISCARD ALL;
DISCARD ALL
regress=> SHOW myapp.username;
 myapp.username 
----------------
 unknown_user
(1 row)

另外,请注意,您不能使用SETSET LOCAL与服务器端绑定参数一起使用。如果要使用绑定参数(“准备好的语句”),请考虑使用函数形式set_config(...)。查看系统管理功能

使用临时表

这种方法需要使用一个触发器(或最好由触发器调用的辅助函数),它试图从每个会话应该具有的临时表中读取一个值。如果找不到临时表,则提供默认值。这可能有点慢。仔细测试。

get_app_user()定义:

CREATE OR REPLACE FUNCTION get_app_user() RETURNS text AS $$
DECLARE
    cur_user text;
BEGIN
    BEGIN
        cur_user := (SELECT username FROM current_app_user);
    EXCEPTION WHEN undefined_table THEN
        cur_user := 'unknown_user';
    END;
    RETURN cur_user;
END;
$$ LANGUAGE plpgsql VOLATILE;

演示:

regress=> CREATE TEMPORARY TABLE current_app_user(username text);
CREATE TABLE
regress=> INSERT INTO current_app_user(username) VALUES ('testuser');
INSERT 0 1
regress=> INSERT INTO tg_demo(blah) VALUES ('42');
NOTICE:  Current user is: testuser
INSERT 0 1
regress=> DISCARD ALL;
DISCARD ALL
regress=> INSERT INTO tg_demo(blah) VALUES ('42');
NOTICE:  Current user is: unknown_user
INSERT 0 1

安全会话变量

还有一个向 PostgreSQL 添加“安全会话变量”的提议。这些有点像包变量。从 PostgreSQL 12 开始,该功能尚未包含在内,但如果您需要,请留意并在黑客名单上发言。

高级:您自己的共享内存区域扩展

对于高级用途,您甚至可以拥有自己的 C 扩展注册共享内存区域,并使用 C 函数调用在 DSA 段中读取/写入值在后端之间进行通信。有关详细信息,请参阅 PostgreSQL 编程示例。您将需要 C 知识、时间和耐心。

于 2012-11-01T07:46:31.570 回答
14

set 有一个这里没有提到的变体集会话。这很可能是应用程序开发人员通常真正想要的,而不是普通的 set 或 set local。

set session trolol.userr = 'Lol';

我的测试触发设置稍微简单一些,但想法与 Craig Ringer 的选项 2 相同。

create table lol (
    pk varchar(3) not null primary key,
    createuser varchar(20) not null);


CREATE OR REPLACE function update_created() returns trigger as $$ 
     begin new.createuser := current_setting('trolol.userr'); return new; end; $$ language plpgsql;


create trigger lol_update before update on lol for each row execute procedure update_created();
create trigger lol_insert before insert on lol for each row execute procedure update_created();

在这一点上,我觉得这是完全可以接受的。如果由于某种原因意外未设置会话变量,则没有 DDL 语句并且插入/更新将不会成功。

使用DISCARD ALL可能不是一个好主意,因为它会丢弃所有内容。例如 SqlKorma 根本不喜欢这个。相反,您可以使用重置变量

SET software.theuser TO DEFAULT

我简要地考虑了第四个选项。在标准变量集中有可以使用的“application_name”。这个解决方案有一些限制,但也有一些明显的优势,具体取决于上下文。

有关第四个选项的更多信息,请参阅:

通过 JDBC 设置 application_name

关于 application_name 的 postgre 文档

于 2013-10-16T18:23:44.860 回答
1

另一种选择是last_updated_user_id在被审计的表中有一个。该值可以由 PHP/Webapp 轻松设置,并将在NEW.last_updated_user_id添加到审计表中可用

于 2020-04-13T13:50:42.623 回答
0

我认为在撰写此答案时,还没有令人满意的解决方案。我最终在 nodejs 中间件上使用了set_config

app.use((req, res, next)=>{
    db.query("SELECT set_config('myapp._user_id', $1, false)", [req.session.user.id]);
    next();
});

调试后发现是全局设置给所有用户的。(竞争条件很容易发生)而且您不能使用SET LOCAL,因为您需要在请求开始时开始事务,并在请求完成时提交/回滚。

于 2020-09-24T20:31:02.533 回答