生产环境下数据库的一个常见要求是能够跟踪用户编辑数据的历史:数据在两个日期之间是如何变化的,是谁操作的,以及它们哪些内容变化了?一些GIS系统通过在客户端接口中包含更改管理功能来跟踪用户的编辑数据操作,但这增加了客户端编辑工具的复杂性。
使用数据库和数据库的触发器机制,可以对任何表进行编辑历史跟踪,从而让客户端保持对编辑表的简单“直接编辑”(客户端不用负责追踪编辑历史的功能,只负责CRUD)。
历史跟踪的工作方式是增加一个记录编辑历史的历史表,为每个编辑操作保留历史记录。历史表包含如下信息:
如果编辑表中创建了一条新记录,则保留新记录添加的时间、操作的用户。
如果编辑表中的一条记录被删除,保留记录被删除的时间、操作的用户。
如果编辑表中的一条记录被更新,则添加删除信息(针对旧状态)和创建信息(针对新状态)。
使用历史表的信息,可以恢复任何时间点编辑表的状态。在本例中,我们将针对nyc_streets表创建历史表,从而追踪nyc_streets表的编辑历史。
1)首先,添加一个新的nyc_streets_history表。这是我们将用来存储所有编辑历史信息的历史表。除了包含nyc_streets表的所有字段外,我们还为历史表增加了五个字段:
hid —— 历史表的主码
created —— 编辑表中对应记录被创建的时间
created_by ——编辑表中对应记录被创建的操作用户
deleted —— 编辑表中对应记录被删除的时间
deleted_by —— 编辑表中对应记录被删除的操作用户
CREATE TABLE nyc_streets_history (
hid SERIAL PRIMARY KEY,
gid INTEGER,
id FLOAT8,
name VARCHAR(200),
oneway VARCHAR(10),
type VARCHAR(50),
geom GEOMETRY(MultiLinestring,26918),
created TIMESTAMP, -- 时间戳(无时区)
created_by VARCHAR(32),
deleted TIMESTAMP,
deleted_by VARCHAR(32)
);
2)接下来,我们将编辑表nyc_streets的当前状态导入到历史表中,这样我们就有一个可以追踪历史编辑的起点。注意,我们插入了创建时间(created)和创建用户(created_by),但删除相关信息为空。
INSERT INTO nyc_streets_history
(gid, id, name, oneway, type, geom, created, created_by)
SELECT gid, id, name, oneway, type, geom, now(), current_user
FROM nyc_streets;
3)现在,我们需要为编辑表创建三个触发器,用于插入、删除和更新操作。首先我们创建触发器函数(PostgreSQL的触发器动作体只能是函数),然后将它们作为触发器动作体绑定到表中。
对于INSERT操作,我们只需记录创建时间(created)和创建用户(created_by)信息并将该新记录添加到历史表中。
CREATE OR REPLACE FUNCTION nyc_streets_insert() RETURNS trigger AS
$$
BEGIN
INSERT INTO nyc_streets_history
(gid, id, name, oneway, type, geom, created, created_by)
VALUES
(NEW.gid, NEW.id, NEW.name, NEW.oneway, NEW.type, NEW.geom,
current_timestamp, current_user);
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER nyc_streets_insert_trigger
AFTER INSERT ON nyc_streets
FOR EACH ROW
EXECUTE PROCEDURE nyc_streets_insert();
对于DELETE操作,我们只要将对应的历史记录(且deleted字段是NULL)标记为已删除。
CREATE OR REPLACE FUNCTION nyc_streets_delete() RETURNS trigger AS
$$
BEGIN
UPDATE nyc_streets_history
SET deleted = current_timestamp, deleted_by = current_user
WHERE deleted IS NULL and gid = OLD.gid;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER nyc_streets_delete_trigger
AFTER DELETE ON nyc_streets
FOR EACH ROW
EXECUTE PROCEDURE nyc_streets_delete();
对于UPDATE操作,我们首先将对应的历史记录标记为已删除,然后插入更新状态的新记录。
CREATE OR REPLACE FUNCTION nyc_streets_update() RETURNS trigger AS
$$
BEGIN
UPDATE nyc_streets_history
SET deleted = current_timestamp, deleted_by = current_user
WHERE deleted IS NULL and gid = OLD.gid;
INSERT INTO nyc_streets_history
(gid, id, name, oneway, type, geom, created, created_by)
VALUES
(NEW.gid, NEW.id, NEW.name, NEW.oneway, NEW.type, NEW.geom,
current_timestamp, current_user);
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER nyc_streets_update_trigger
AFTER UPDATE ON nyc_streets
FOR EACH ROW
EXECUTE PROCEDURE nyc_streets_update();
现在启用了历史表,我们可以对编辑表进行编辑,并在历史表中看到日志条目。
请注意这种数据库支持的追踪编辑历史的强大功能:无论使用什么工具进行编辑,比如SQL命令行、基于Web的JDBC工具,还是像QGIS这样的桌面工具,编辑历史都会被持续追踪。
让我们把这两条名字叫做“Cumberland Walk”的街道改成更时髦的“Cumberland Wynde”:
UPDATE nyc_streets SET name = 'Cumberland Wynde'
WHERE name = 'Cumberland Walk';
更新这两条街道将会把历史表中原来的街道标记为已删除,删除时间为现在,以及再添加具有新名称的两条新街道。
SELECT hid, gid, name, created, created_by, deleted, deleted_by
FROM nyc_streets_history
WHERE name = 'Cumberland Walk' OR name = 'Cumberland Wynde'
ORDER BY hid;
既然我们有了历史表,那还有什么用处呢?它对"时间旅行"很有用!要旅行到特定的时刻T(即查看T时刻历史表的状态),我们需要构造一个查询,查询出符合如下规则的记录:
所有在时刻T之前创建且目前尚未删除的记录。
所有在时刻T之前创建,但在T之后删除的记录。
我们可以使用这个逻辑来创建过去某个时刻T对应的数据状态的视图。假设我们的所有测试编辑都发生在过去30分钟内,那我们可以创建一个30分钟前历史表数据状态的视图:
– 30分钟前的历史表的数据状态
– 记录必须在30分钟前创建,并且现在没被删除(DELETED为NULL)
– 或在过去30分钟内已删除的
CREATE OR REPLACE VIEW nyc_streets_thirty_min_ago AS
SELECT * FROM nyc_streets_history
WHERE created < (now() - '30min'::interval)
AND ( deleted IS NULL OR deleted > (now() - '30min'::interval) );
我们还可以创建视图来显示被特定用户(比如用户postgres)编辑的记录:
CREATE OR REPLACE VIEW nyc_streets_postgres AS
SELECT * FROM nyc_streets_history
WHERE created_by = 'postgres' OR deleted_by = 'postgres';
PostgreSQL触发器官方文档
PostgreSQL过程化语言(PL/pgSQL)官方文档