Как заставить 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;