考虑下表。
CREATE TABLE subscriptions
(
id SERIAL PRIMARY KEY,
state text NOT NULL CHECK (state IN ('trial', 'expired', 'active', 'cancelled')),
created_at timestamptz NOT NULL
);
CREATE TABLE subscriptions_history (LIKE subscriptions);
ALTER table subscriptions_history ADD COLUMN change_type CHARACTER VARYING(6);
ALTER table subscriptions_history ADD COLUMN change_time timestamp without time zone;
ALTER TABLE subscriptions_history ADD CONSTRAINT subscriptions_history_change_type_check CHECK ((((change_type)::text = 'INSERT'::text) OR ((change_type)::text = 'UPDATE'::text) OR ((change_type)::text = 'DELETE'::text)));
我编写了以下触发器函数,它会在
subscriptions_history
表中的每次更改(插入、更新、删除)时向 subscriptions
插入一行。
CREATE OR REPLACE FUNCTION add_to_subscriptions_history()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
AS $$
DECLARE
current_row RECORD;
BEGIN
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
current_row := NEW;
ELSIF (TG_OP = 'DELETE') THEN
current_row := OLD;
END IF;
INSERT INTO subscriptions_history (id, state, created_at, change_type, change_time) VALUES (current_row.id, current_row.state, current_row.created_at, TG_OP, now());
RETURN current_row;
END;
$$;
触发:
DROP TRIGGER IF EXISTS subscriptions_trigger ON subscriptions;
CREATE TRIGGER subscriptions_trigger
BEFORE INSERT OR DELETE OR UPDATE ON subscriptions
FOR EACH ROW EXECUTE PROCEDURE add_to_subscriptions_history();
这个实现工作正常,但是我有很多表需要这样的历史记录,并且我不想为每个表编写一个触发器函数。我想概括这个函数
add_to_subscriptions_history()
,以便它可以处理任何表。
我唯一的想法是以某种方式将表及其历史表作为参数提供给触发器函数,并在插入期间迭代记录列。 我该怎么做?还有其他想法、脚本吗?
虽然 Adrian 在第一条评论中已经回答了这个问题,而且这个问题已经有 4 年历史了,但我想详细说明一下,这样即使链接的博客文章消失了,答案也不会丢失。
正如您所写,想法是以目标表作为参数来调用触发器函数。 然后可以在
INSERT
块内动态构造 EXECUTE() ... USING
。
但是,如果您为每个历史表假设相同的命名方案,您也可以动态创建
INSERT
而不需要给出任何参数。
两者结合起来,看起来基本上是这样的:
CREATE OR REPLACE FUNCTION add_to_history()
RETURNS TRIGGER
SET SCHEMA 'public' -- always public? Otherwise use TG_TABLE_SCHEMA
LANGUAGE plpgsql
AS $$
DECLARE
current_row RECORD;
history_table text := TG_ARGV[0];
BEGIN
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
current_row := NEW;
ELSIF (TG_OP = 'DELETE') THEN
current_row := OLD;
END IF;
IF history_table IS NULL THEN
history_table := TG_TABLE_NAME || '_history';
END IF;
EXECUTE 'INSERT INTO ' ||
quote_ident(history_table) ||
'(id, state, created_at, change_type, change_time) ' ||
' VALUES ( $1, $2, $3, $4, now() )'
USING current_row.id, current_row.state, current_row.created_at, TG_OP;
RETURN current_row;
END;
$$;
需要
quote_ident
才能正确引用参数才能用作 SQL 标识符。
如果您需要审计的表的结构总是像上面这样,那么您应该没问题。 对于更通用且更复杂的解决方案,请查看“temporal_tables”versioning_function。这假设主表和历史表的结构相同,但主表的结构可能不同(例如 PK 列名称)。最重要的是,它使您可以选择根据特定时间戳查询表的内容。