我已经考虑了一段时间,只能想到两种方法来做到这一点。当制作成抽象数据层/模型时,两者都可以完全透明地工作。
顺便说一句,ORM 映射器原则中有一个“版本化”表数据的实现。在他们的文档中查看这个例子。也许这符合你的需要,但它不适合我的。删除原始记录时似乎会删除所有历史数据,使其不是真正的修订安全。
选项 A:拥有每个表的副本以保存修订数据
假设您有一个简单的联系人表:
CREATE TABLE contact (
id INT NOT NULL auto_increment,
name VARCHAR(255),
firstname VARCHAR(255),
lastname VARCHAR(255),
PRIMARY KEY (id)
)
您将创建该表的副本并添加修订数据:
CREATE TABLE contact_revisions (
id INT NOT NULL,
name VARCHAR(255),
firstname VARCHAR(255),
lastname VARCHAR(255),
revision_id INT auto_increment,
type ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL,
change_time DEFAULT current_timestamp,
PRIMARY KEY(revision_id)
)
跟踪INSERT
和UPDATE
使用AFTER
触发器。在原始数据的每个新数据修订中,在修订表中插入新数据的副本并type
正确设置修改。
要记录DELETE
修订安全,您还必须在历史表中插入新行!为此,您应该使用BEFORE DELETE
触发器并在删除最新值之前存储它们。否则,您还必须删除NOT NULL
历史表中的每个约束。
关于此实现的一些重要说明
- 对于历史表,您必须从修订表中删除每个
UNIQUE KEY
(此处:PRIMARY KEY
),因为对于每个数据修订,您将多次拥有相同的键。
- 当您
ALTER
通过更新(例如软件更新)对原始表中的模式和数据进行更新时,您必须确保将相同的数据或模式更正应用于历史表及其数据。否则,您在恢复到记录集的旧版本时会遇到麻烦。
- 在现实世界的实现中,您可能想知道哪个用户修改了数据。为了保证修订安全,永远不要从 users 表中删除用户记录。您应该只使用标志设置禁用帐户。
- 通常,单个用户操作涉及多个表。在现实世界的实现中,您还必须跟踪多个表中的哪些更改属于单个用户事务以及以何种顺序。在实际用例中,您可能希望以相反的顺序一起还原单个事务的所有更改。这将需要一个额外的修订表来跟踪用户和事务,并与历史表中的所有这些单独的修订保持松散的关系。
好处:
- 完全在数据库中,独立于应用程序代码。(好吧,当跟踪用户事务很重要时并不重要。这将需要单个查询范围之外的一些逻辑)
- 所有数据都是原始格式,没有隐式类型转换。
- 在修订中的搜索性能良好
- 轻松回滚。只需在原始表上执行一个简单的
INSERT .. ON DUPLICATE KEY UPDATE ..
语句,使用您要回滚的修订版中的数据。
优点:
- 很难手动实现。
- 在数据库迁移/应用程序更新方面很难(但并非不可能)实现自动化。
如上所述,教义versionable
也有类似的作用。
选项 B:有一个中央更改日志表
前言:不好的做法,仅用于说明替代方案。
这种方法确实严重依赖应用程序逻辑,它应该隐藏在数据层/模型中。
您有一个中央历史记录表来跟踪
- 谁干的
- 什么时候
- 修改、插入或删除
- 什么数据
- 在哪个领域
- 哪张桌子
与其他方法一样,您可能还希望跟踪哪些单独的数据更改属于单个用户操作/事务以及以何种顺序。
好处:
- 向表中添加字段或创建新表时无需与原始表保持同步。它透明地缩放。
优点:
- 使用简单值 = 数据库中的密钥存储的不良做法
- 由于隐式类型转换,搜索性能不佳
- 当中央历史表因写锁而成为瓶颈时,可能会降低应用程序/数据库的整体性能(这仅适用于具有表锁的特定引擎,即 MyISAM)
- 实现回滚要困难得多
- 由于隐式类型转换可能导致的数据转换错误/精度损失
- 当您直接访问代码中某处的数据库而不是使用模型/数据层时,不会跟踪更改,并且忘记在这种情况下您必须手动写入修订日志。与其他程序员一起工作时可能是一个大问题。
结论:
- 选项 B对于小型应用程序来说非常方便,因为它只是用于记录更改时的简单“插入”。
- 如果您想及时返回并能够轻松比较历史修订版123与修订版125之间的差异和/或恢复到旧数据,那么选项 A是艰难的选择。