我们在项目中要求存储数据库中实体的所有修订(更改历史记录).目前我们有2个设计方案:
例如,对于"员工"实体
设计1:
-- Holds Employee Entity "Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)" -- Holds the Employee Revisions in Xml. The RevisionXML will contain -- all data of that particular EmployeeId "EmployeeHistories (EmployeeId, DateModified, RevisionXML)"
设计2:
-- Holds Employee Entity "Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)" -- In this approach we have basically duplicated all the fields on Employees -- in the EmployeeHistories and storing the revision data. "EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName, LastName, DepartmentId, .., ..)"
有没有其他办法做这件事?
"设计1"的问题在于,每次需要访问数据时我们都必须解析XML.这将减慢进程并添加一些限制,例如我们无法在修订数据字段上添加联接.
而"设计2"的问题在于我们必须复制所有实体上的每个字段(我们有大约70-80个实体,我们希望对其进行修改).
我认为这里要问的关键问题是"谁/什么将使用历史"?
如果它主要用于报告/人类可读历史,我们过去实施了这个方案......
创建一个名为'AuditTrail'的表或具有以下字段的表...
[ID] [int] IDENTITY(1,1) NOT NULL, [UserID] [int] NULL, [EventDate] [datetime] NOT NULL, [TableName] [varchar](50) NOT NULL, [RecordID] [varchar](20) NOT NULL, [FieldName] [varchar](50) NULL, [OldValue] [varchar](5000) NULL, [NewValue] [varchar](5000) NULL
然后,您可以向所有表添加"LastUpdatedByUserID"列,每次在表上执行更新/插入时都应该设置这些列.
然后,您可以为每个表添加一个触发器,以捕获发生的任何插入/更新,并在此表中为每个已更改的字段创建一个条目.由于该表还为每个更新/插入提供了"LastUpdateByUserID",因此您可以在触发器中访问此值,并在添加到审计表时使用它.
我们使用RecordID字段来存储正在更新的表的键字段的值.如果它是一个组合键,我们只是在字段之间用'〜'进行字符串连接.
我敢肯定这个系统可能有缺点 - 对于大量更新的数据库,性能可能会受到影响,但对于我的网络应用程序,我们获得的读取次数多于写入次数,而且似乎表现相当不错.我们甚至编写了一个小的VB.NET实用程序来根据表定义自动编写触发器.
只是一个想法!
千万不能把它们都放在一个表与IsCurrent鉴别属性.这只会导致问题,需要代理键和各种其他问题.
设计2确实存在架构更改问题.如果更改Employees表,则必须更改EmployeeHistories表以及与之一起使用的所有相关sproc.可能会使您的架构更改工作量增加一倍.
设计1运行良好,如果正确完成,在性能损失方面成本不高.您可以使用xml架构甚至索引来克服可能的性能问题.您关于解析xml的注释是有效的,但您可以使用xquery轻松创建视图 - 您可以将其包含在查询中并加入.像这样......
CREATE VIEW EmployeeHistory AS , FirstName, , DepartmentId SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName, RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName, RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId, FROM EmployeeHistories
Database Programmer中的History Tables文章博客中可能很有用 - 涵盖了此处提出的一些要点并讨论了增量的存储.
编辑
在历史表论文中,作者(Kenneth Downs)建议维护至少七列的历史表:
变更的时间戳,
进行更改的用户
用于标识已更改记录的标记(其中历史记录与当前状态分开维护),
更改是插入,更新还是删除,
旧的价值,
新的价值,
delta(对数值的更改).
不应在历史表中跟踪永不更改或不需要其历史记录的列以避免膨胀.存储数值的增量可以使后续查询更容易,即使它可以从旧值和新值派生.
历史表必须是安全的,禁止非系统用户插入,更新或删除行.应仅支持定期清除以减小总体大小(如果用例允许).
避免设计1; 一旦您需要回滚到旧版本的记录 - 使用管理员控制台自动或"手动",它就不是很方便了.
我没有真正看到Design 2的缺点.我认为第二个,History表应该包含第一个记录表中的所有列.例如,在mysql中,您可以轻松地创建与另一个表(create table X like Y
)具有相同结构的表.而且,当您要在实时数据库中更改记录表的结构时,您仍然必须使用alter table
命令 - 并且您也无需为历史记录表运行这些命令.
笔记
记录表仅包含最新修订;
历史表包含Records表中所有以前的记录修订版;
历史表的主键是Records表的主键,添加了RevisionId
列;
考虑其他辅助字段,例如ModifiedBy
- 创建特定修订版的用户.您可能还希望有一个字段DeletedBy
来跟踪删除特定修订的人员.
想一想DateModified
应该是什么意思 - 要么意味着创建了这个特定的修订版,要么意味着当这个特定版本被另一个版本替换时.前者要求字段位于记录表中,并且第一眼看上去似乎更直观; 但是,对于删除的记录(删除此特定修订版的日期),第二个解决方案似乎更实用.如果您选择第一个解决方案,您可能需要第二个字段DateDeleted
(当然,只有当您需要它时).取决于你和你真正想要记录的内容.
设计2中的操作非常简单:
修改
将记录从Records表复制到History表,给它新的RevisionId(如果它不存在于Records表中),处理DateModified(取决于你如何解释它,见上面的注释)
继续正常更新记录表中的记录
删除
与修改操作的第一步完全相同.根据您选择的解释,相应地处理DateModified/DateDeleted.
取消删除(或回滚)
从History表中获取最高(或某些特定?)版本并将其复制到Records表
列出特定记录的修订历史记录
从历史记录表和记录表中选择
想想你对这次行动的期望是什么; 它可能会确定您从DateModified/DateDeleted字段中需要哪些信息(参见上面的注释)
如果你去设计2,那么所需的所有SQL命令都将非常简单,以及维护!也许,如果你在Records表中使用辅助列(RevisionId
,DateModified
)也会容易得多,以使两个表保持完全相同的结构(除了唯一键)!这将允许简单的SQL命令,它将容忍任何数据结构更改:
insert into EmployeeHistory select * from Employe where ID = XX
别忘了使用交易!
至于扩展,这个解决方案是非常有效的,因为你不会来回转换XML中的任何数据,只是复制整个表行 - 非常简单的查询,使用索引 - 非常有效!
我们已经实现了一个非常类似于Chris Roberts建议的解决方案的解决方案,这对我们来说非常有效.
唯一的区别是我们只存储新值.旧值毕竟存储在上一个历史记录行中
[ID] [int] IDENTITY(1,1) NOT NULL, [UserID] [int] NULL, [EventDate] [datetime] NOT NULL, [TableName] [varchar](50) NOT NULL, [RecordID] [varchar](20) NOT NULL, [FieldName] [varchar](50) NULL, [NewValue] [varchar](5000) NULL
假设您有一个包含20列的表格.这样,您只需存储已更改的确切列,而不必存储整行.
如果您必须存储历史记录,请创建一个与您正在跟踪的表格具有相同架构的影子表,以及"修订日期"和"修订版类型"列(例如"删除","更新").写入(或生成 - 见下文)一组触发器来填充审计表.
制作一个能够读取表的系统数据字典的工具并生成一个创建影子表的脚本和一组用于填充它的触发器,这是相当简单的.
不要尝试使用XML,XML存储的效率远低于此类触发器使用的本机数据库表存储.
Ramesh,我参与了基于第一种方法的系统开发.
事实证明,将修订版存储为XML会导致数据库的巨大增长并显着减慢速度.
我的方法是每个实体有一个表:
Employee (Id, Name, ... , IsActive)
其中IsActive是最新版本的标志
如果要将一些其他信息与修订相关联,可以创建包含该信息的单独表,并使用PK\FK关系将其与实体表链接.
这样,您可以将所有版本的员工存储在一个表中.这种方法的优点:
简单的数据库结构
没有冲突,因为表变为仅附加
只需更改IsActive标志即可回滚到以前的版本
无需连接即可获取对象历史记录
请注意,您应该允许主键不唯一.
我过去看过这种方式的方式是有的
Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );
你永远不会在这个表上"更新"(除了更改isCurrent的有效),只需插入新行.对于任何给定的EmployeeId,只有1行可以具有isCurrent == 1.
维护它的复杂性可以通过视图和"而不是"触发器隐藏(在oracle中,我假设其他RDBMS类似的东西),如果表太大而无法通过索引处理,您甚至可以转到物化视图) .
这种方法没问题,但最终可能会出现一些复杂的查询.
就我个人而言,我非常喜欢你的Design 2方式,这也是我过去的做法.它易于理解,易于实现且易于维护.
它还为数据库和应用程序创建了很少的开销,尤其是在执行读取查询时,这很可能是99%的时间.
自动创建历史表和触发器进行维护也很容易(假设它将通过触发器完成).