Как заставить PostgreSQL вставить строку в таблицу при удалении из другой таблицы?

У нас есть приложение, которое удалит строку из таблицы на основе пользовательских запросов. Я не могу изменить код приложения. Однако я хочу вставить строку в другую таблицу (вроде журнала журнала) с информацией из нескольких других таблиц на основе информации о удаляемой строке.

Как достичь этого в PostgreSQL?

Ответ 1

Введите триггерную функцию. Что-то вроде этого:

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();

Ключевые элементы

  • Лучше всего сделать триггер AFTER DELETE и FOR EACH ROW.

  • Чтобы вернуть все столбцы из старой таблицы, используйте синтаксис (OLD).*. См. Руководство по доступа к составным типам. В качестве альтернативы OLD.* также является синтаксисом, потому что OLD неявно добавляется в предложение FROM. Для выражения VALUES оно должно быть (OLD).*. Как:

    INSERT INTO other_tbl
    VALUES((OLD).*, some_variable)
    
  • Вы можете включать значения из любой другой таблицы, как я демонстрирую. Просто убедитесь, что вы получите одну строку или создаете несколько записей.

  • Когда триггер запускает событие AFTER, функция может RETURN NULL.


О видимости

В ответ на @couling бдительный комментарий.

В то время как внешние ключи могут быть объявлены как DEFERRED, это отложит проверку целостности, а не удаление. Строки, которые удаляются в триггерах, выполненных до того, как находится под рукой, или через ON DELETE CASCADE внешние ключи будут больше не отображаться во время вызова этого триггера AFTER DELETE. (Все это происходит в одной транзакции, очевидно. Ни одна из этих деталей не имеет значения для других транзакций, которые будут видеть все или ничто из эффектов. Подробнее см. В руководстве для Модель MVCC и изоляция транзакций.)

Поэтому, если вы хотите включать значения из строк в зависимости от вашего пути в INSERT, обязательно вызовите этот триггер до, эти строки будут удалены.

Возможно, вам придется выполнить этот триггер BEFORE DELETE.

Или это может означать, что вам нужно заказывать свои триггеры соответственно, триггеры BEFORE приходят до триггеров AFTER, очевидно. И триггеры на том же уровне выполняются в в алфавитном порядке.

Однако, если я здесь слишком точен, могу добавить, что изменения, внесенные в строку (или зависящие строки) в других триггерах BEFORE, также видны только в том случае, если они вызываются до этого.

Мой совет сделать его триггером AFTER состоял в том, что он менее подвержен осложнениям и дешевле, если другой триггер может отменить (откат) DELETE на полпути через операцию - если ни одно из вышеприведенных положений не применяется.

Ответ 2

Вы можете использовать функции, которые я написал для хранения исторических данных. Сокращение:

Исторические данные хранятся в отдельной схеме с именем аудита. Итак, первым шагом будет создание этой схемы:

CREATE SCHEMA audit;

В схеме аудита можно найти точную копию таблиц публично, которые создаются динамически, когда происходит первое изменение данных в публичной схеме. Таким образом, перед первым использованием схемы аудита базы данных остается пустой, пока пользователь не выполнит свою первую вставку в одну из таблиц.

Функция _audit_table_creator (имя) копирует структуру таблицы из общедоступной схемы и создает ту же таблицу в схеме аудита с некоторыми дополнительными столбцами, которые я назвал "отметкой аудита". Аудит печати содержит информацию о:

  • время, когда запись была удалена (shift_time),
  • пользователь, который сделал удаление (who_altered),
  • 'УДАЛИТЬ штамп (alter_type) и
  • который был изменен - ​​только для операций обновления (changed_columns);

Я думаю, что самым большим преимуществом этого решения является поддержка составных первичных ключей (функция _where_clause_creator (текст []) создает правильное предложение where для таблицы, вызываемой триггером, путем конкатенации строк в правильном порядке);

Просмотр исторических записей:

Каждый раз, когда мы хотим получить архивные данные, мы должны использовать псевдонимы, т.е. извлекать исторические данные о пользователе whos user_id = 5, которые нужно написать:

SELECT * FROM audit.users WHERE user_id = 5; 

Таким образом, одни и те же запросы могут использоваться в обеих схемах, но для извлечения исторических данных необходимо добавить "аудит". перед именем таблицы.

Вы можете создать триггеры удаления автоматически для всех таблиц в базе данных сразу, если вы можете просто выполнить запрос:

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;