将用户标识传递给PostgreSQL触发器

pip*_*ipo 37 postgresql triggers

我正在使用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
Run Code Online (Sandbox Code Playgroud)

Cra*_*ger 43

选项包括:

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

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

  • 调整应用程序,使其具有每个应用程序用户的数据库角色.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();
Run Code Online (Sandbox Code Playgroud)

使用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;
Run Code Online (Sandbox Code Playgroud)

使用SET LOCALfor transaction-local current username进行演示:

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)
Run Code Online (Sandbox Code Playgroud)

如果您使用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)
Run Code Online (Sandbox Code Playgroud)

使用临时表

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

SET定义:

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;
Run Code Online (Sandbox Code Playgroud)

演示:

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
Run Code Online (Sandbox Code Playgroud)


lok*_*ori 10

set有一个未在此处提及的变体集会话.这很可能是应用程序开发人员通常真正想要的,而不是普通集或本地集.

set session trolol.userr = 'Lol';
Run Code Online (Sandbox Code Playgroud)

我的测试触发器设置稍微简单一点,但这个想法与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();
Run Code Online (Sandbox Code Playgroud)

我觉得这在目前是可以接受的.如果由于某种原因意外未设置会话变量,则不会有DDL语句和插入/更新.

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

SET software.theuser TO DEFAULT
Run Code Online (Sandbox Code Playgroud)

我简要考虑了第四种选择.在标准变量集中,可以使用"application_name".该解决方案具有一些局限性,但取决于上下文也具有一些明显的优点.

有关此第四个选项的更多信息,请参阅以下内容:

通过JDBC设置application_name

application_name上的postgre文档

  • 根据文档,SET SESSION等同于简单的SET.它只是一种更明确的方式来指定SET的范围,可以是LOCAL或SESSION,默认为SESSION.资料来源:http://www.postgresql.org/docs/9.4/static/sql-set.html (2认同)