如何使PostgreSQL在从另一个表中删除时向表中插入一行?

时间:2012-03-03 16:25:05

标签: postgresql triggers plpgsql

我们有一个应用程序,它将根据用户请求从表中删除一行。我无法更改应用程序代码。但是,我想根据正在删除的行的信息,将一行插入到另一个表(有点像日志日志)中,其中包含来自其他几个表的信息。

如何在PostgreSQL中实现这一目标?

2 个答案:

答案 0 :(得分:14)

编写触发功能。像这样:

CREATE OR REPLACE FUNCTION trg_backup_row()
  RETURNS trigger AS
$BODY$
BEGIN

INSERT INTO other_tbl
SELECT (OLD).*, t.other_col                -- all columns of from old table
-- SELECT OLD.col1, OLD.col2, t.other_col  -- alternative: some cols from old tbl
FROM   third_tbl t
WHERE  t.col = OLD.col  -- link to third table with info from deleted row
AND    <unique_condition_to_avoid_multiple_rows_if_needed>;

RETURN NULL;

END;
$BODY$
  LANGUAGE plpgsql VOLATILE;

触发器ON DELETE。像这样:

CREATE TRIGGER delaft
  AFTER DELETE
  ON tbl
  FOR EACH ROW
  EXECUTE PROCEDURE trg_backup_row();

关键要素

  • 最好将其设为trigger AFTER DELETE and FOR EACH ROW

  • 要返回旧表中的所有列,请使用语法(OLD).*。请参阅有关accessing composite types的手册。或者,OLD.*也是有效的语法,因为OLD隐式添加到FROM子句中。但是对于VALUES表达式,它必须是(OLD).*。像:

    INSERT INTO other_tbl
    VALUES((OLD).*, some_variable)
    
  • 您可以包含我演示的任何其他表格中的值。只需确保获得一行,或者您创建多个条目。

  • 当触发器触发AFTER事件时,该函数可以RETURN NULL


关于可见性

回应@ couling的注意评论。

虽然外键可以是declared as DEFERRED,但这只会推迟完整性检查,而不是删除本身。在{{1>}外键的之前执行的触发器中删除的行将在ON DELETE CASCADE不再可见触发器被调用。 (这一切都明显发生在一个交易中。这些细节都不适用于其他交易,这些交易将看到全部或全部效果。有关MVCC model and transaction isolation的更多信息,请参阅手册。)

因此,如果您希望在AFTER DELETE中包含来自行的值,请务必在之前调用此触发器

您可能需要触发INSERT

或者它可能意味着您必须相应地订购触发器,显然BEFORE DELETE触发器会在BEFORE触发之前触发。同一级别的触发器在alphabetical order中执行。

但是,只要我在这里非常精确,我可能还会补充说,对其他AFTER触发器中的行(或依赖行)所做的更改也只有在之前调用时才可见/ em>这一个。

我建议将它设为BEFORE触发器是因为它不太容易出现并发症,如果其他触发器可能在操作中途取消(回滚)AFTER,则不会更容易出现 - 只要没有以上适用。

答案 1 :(得分:4)

您可能希望使用我为保留历史数据而编写的函数。简短描述:

历史数据保存在名为审核的单独模式中。因此,第一步是创建此架构:

CREATE SCHEMA audit;

在审计模式中,可以从公共中找到表的精确副本,这些表是在公共模式中的数据发生第一次更改时动态创建的。因此,在第一次使用数据库之前,审计模式保持为空,直到用户将第一次插入到其中一个表中。

函数_audit_table_creator(name)正在从公共模式复制表的结构,并在审计模式中创建具有一些其他列的相同表,我称之为“审计标记”。审计标记保存有关的信息:

  • 删除记录的时间(shift_time),
  • 进行删除的用户(who_altered),
  • 'DELETE'标记(alter_type)和
  • 已更改的列 - 仅用于更新操作(changed_columns);

我认为此解决方案的最大优势是支持 复合主键 (函数_where_clause_creator(text [])为触发器调用的表创建适当的where子句以正确的顺序连接字符串);

查看历史记录

每当我们想要检索档案数据时,我们必须使用别名,即检索有关user_id = 5的用户的历史数据:

SELECT * FROM audit.users WHERE user_id = 5; 

因此,可以在两个模式中使用相同的查询,但要检索历史数据,必须在表名之前添加“audit。”。

您可能希望一次为数据库中的所有表自动创建删除触发器,如果​​您这样做,则可以执行查询:

SELECT * FROM  audit_gen_triggers();

主要功能:

CREATE OR REPLACE FUNCTION audit_delete() 
  RETURNS trigger AS
$BODY$DECLARE
t_name text;                 
query_op text;              
primary_keys text;           
c record;
key_arr text;
keys_arr text;
p_r text;

    BEGIN

    t_name := 'audit.' || TG_TABLE_NAME;
         IF NOT EXISTS(SELECT 1 FROM pg_tables WHERE schemaname = 'audit' AND 
                       tablename = TG_TABLE_NAME) THEN
           EXECUTE 'SELECT _audit_table_creator(table_name := ($1)::name)' 
           USING TG_TABLE_NAME;
        END IF; 

        FOR c IN SELECT pg_attribute.attname
                      FROM pg_index, pg_class, pg_attribute 
                      WHERE 
                      pg_class.oid = TG_TABLE_NAME::regclass AND
                      indrelid = pg_class.oid AND
                      pg_attribute.attrelid = pg_class.oid AND
                      pg_attribute.attnum = ANY(pg_index.indkey) AND 
                      indisprimary LOOP

               key_arr := c.attname || ', ($1).' || c.attname;
               keys_arr := concat_ws(',', keys_arr, key_arr);
               END LOOP;
               keys_arr := '{' || keys_arr || '}';

        EXECUTE 'SELECT _where_clause_creator(VARIADIC ($1)::text[])' 
        INTO p_r USING keys_arr;
        -- raise notice 'tablica where: %', p_r;

        -- zapisz do tabeli audytowanej wszystkie usuniete wartosci
        query_op := 'INSERT INTO '||  t_name ||
                           ' SELECT NEXTVAL(''serial_audit_' 
                             ||  TG_TABLE_NAME ||'''::regclass),
                             CURRENT_USER, ''' ||  TG_OP || ''',
                             NULL,
                             NOW(),
                             ($1).*
                             FROM ' || TG_TABLE_NAME ||
                             ' WHERE  ' || p_r;
                EXECUTE query_op USING OLD;
        RETURN OLD;
    END;

$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;

触发:

CREATE TRIGGER table_name_delete_audit
  BEFORE DELETE
  ON table_name
  FOR EACH ROW
  EXECUTE PROCEDURE audit_delete();

使用的其他功能:

    CREATE OR REPLACE FUNCTION _array_position(anyarray, anyelement)
      RETURNS integer AS
    $BODY$
    SELECT i
       FROM (SELECT generate_subscripts($1, 1) as i, unnest($1) as v) s
      WHERE v = $2
      UNION ALL
      SELECT 0 
      LIMIT 1;
    $BODY$
      LANGUAGE sql STABLE
      COST 100;


CREATE OR REPLACE FUNCTION _audit_table_creator(table_name name)
RETURNS void AS
$BODY$
DECLARE
query_create text; 

BEGIN
query_create := 'DROP TABLE IF EXISTS temp_insert; 
                 DROP TABLE IF EXISTS temp_insert_prepared';
EXECUTE query_create;

query_create := 'DROP SEQUENCE IF EXISTS serial_audit_' ||  table_name;
                 EXECUTE query_create;
query_create := 'CREATE SEQUENCE serial_audit_' || table_name || ' START 1; 
                 ALTER TABLE serial_audit_' || table_name || 
                 ' OWNER TO audit_owner;';
                 EXECUTE query_create;
query_create := 'CREATE TEMPORARY TABLE temp_insert_prepared ( '
                 || table_name || '_audit_id bigint DEFAULT 
                 nextval(''serial_audit_' || table_name || '''::regclass),
                 who_altered text DEFAULT CURRENT_USER,
                 alter_type varchar(6) DEFAULT ''INSERT'',
                 changed_columns text,
                 shift_time timestamp(0) without time zone DEFAULT NOW(),
                 PRIMARY KEY(' || table_name || '_audit_id )) ON COMMIT DROP';

EXECUTE query_create;

query_create := 'CREATE TEMPORARY TABLE temp_insert ON COMMIT DROP AS TABLE 
                ' || table_name;
                EXECUTE query_create;

                query_create := 'CREATE TABLE audit.' || table_name || 
                ' AS SELECT a.*, b.* FROM temp_insert_prepared a, temp_insert b 
                 WITH NO DATA';
                EXECUTE query_create;

    END;
    $BODY$
      LANGUAGE plpgsql VOLATILE
      COST 100;

CREATE OR REPLACE FUNCTION _where_clause_creator(VARIADIC keys_given text[])
RETURNS text AS
$BODY$
DECLARE
x text;
where_clause text;

BEGIN
FOREACH x IN ARRAY keys_given LOOP
IF ((SELECT _array_position(keys_given, x))%2) <> 0 THEN
where_clause := concat_ws(' AND ', where_clause, x);
ELSE
       where_clause := concat_ws(' = ', where_clause, x);
END IF;
END LOOP;
RETURN where_clause;
END;
$BODY$
  LANGUAGE plpgsql STABLE
  COST 100;



CREATE OR REPLACE FUNCTION audit_gen_triggers()
  RETURNS void AS
$BODY$
DECLARE
r record;
query_create text;
BEGIN
FOR r IN SELECT table_name
             FROM information_schema.tables
             WHERE table_schema = current_schema AND
                         table_type = 'BASE TABLE' LOOP

query_create := 'DROP TRIGGER IF EXISTS ' || r.table_name || '_delete_audit ON ' 
                 || r.table_name || ' CASCADE;
                 CREATE TRIGGER ' || r.table_name || '_delete_audit
                 BEFORE DELETE
                 ON ' || r.table_name || '
                 FOR EACH ROW
                 EXECUTE PROCEDURE audit_delete();';

EXECUTE query_create;
END LOOP;

END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;