Как заставить PostgreSQL вставлять строку в таблицу при удалении из другой таблицы? - PullRequest
11 голосов
/ 03 марта 2012

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

Как мне добиться этого в PostgreSQL?

Ответы [ 2 ]

14 голосов
/ 03 марта 2012

Написать функцию триггера.Примерно так:

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 на полпути операции - при условии, что ничего из вышеперечисленного не применимо.

4 голосов
/ 28 декабря 2012

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

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

CREATE SCHEMA audit;

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

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

  • времени удаления записи (shift_time),
  • пользователе, который сделал удаление (who_altered),
  • 'DELETE' штампе(alter_type) и
  • столбец, который был изменен - ​​только для операций обновления (change_columns);

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

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

Каждый раз, когда мы хотим получить архивные данные, мы должны использовать псевдонимы, то есть, чтобы извлечь исторические данные о пользователе, у которого 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;
...