数据库devise的修订?
我们在项目中要求存储数据库中实体的所有修订(更改历史logging)。 目前我们有2个devisescheme:
例如“员工”实体
devise1:
-- 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)"
devise2:
-- 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, .., ..)"
有没有其他方式做这件事?
“devise1”的问题在于,每次需要访问数据时,都必须parsingXML。 这会减慢进程速度,并增加一些限制,例如我们无法在修订数据字段中添加联接。
而“devise2”的问题是我们必须复制所有实体的每一个领域(我们有大约70-80个我们想要维护的实体)。
- 不要把它放在一个带有IsCurrent descriminator属性的表中。 这只会导致问题,需要代理键和其他各种问题。
- devise2确实有模式更改的问题。 如果更改Employees表,则必须更改EmployeeHistories表和所有与之相关的sprocs。 可能会使模式更改工作倍增。
- devise1运行良好,如果做得好,在性能方面没有太大的成本。 你可以使用一个XML模式,甚至索引来解决可能的性能问题。 您对parsingxml的评论是有效的,但您可以使用xquery轻松创build视图 – 您可以将其包含在查询中并join。 像这样的东西…
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
我想在这里问的关键问题是“谁/将会怎样使用历史”?
如果这主要是报告/人类可读的历史,我们已经实施了这个计划在过去…
创build一个名为“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”列添加到您每次在表上进行更新/插入时应设置的所有表中。
然后,您可以为每个表添加一个触发器来捕获所发生的任何插入/更新,并为每个已更改的字段在此表中创build一个条目。 因为每个更新/插入表也都提供了“LastUpdateByUserID”,所以可以在触发器中访问这个值,并在添加到审计表时使用它。
我们使用RecordID字段来存储正在更新的表的关键字段的值。 如果它是一个组合键,我们只是在字段之间用string“〜”进行string连接。
我敢肯定,这个系统可能有缺点 – 对于数据量大的数据库来说,性能可能会受到影响,但是对于我的networking应用程序来说,我们读取的数量比写入数量多,而且performance也相当不错。 我们甚至写了一个小小的VB.NET工具来根据表定义自动编写触发器。
只是一个想法!
数据库程序员博客中的历史表文章可能是有用的 – 涵盖了这里提出的一些要点,并讨论了三angular洲的存储。
编辑
在“ 历史表”文章中,作者( Kenneth Downs )build议保留至less七列历史表:
- 时间戳的变化,
- 做出改变的用户,
- 用于标识已更改logging的标记(历史logging与当前状态分开保存),
- 无论更改是插入,更新还是删除,
- 旧的价值,
- 新的价值,
- 增量(用于更改数值)。
不要在历史logging表中追踪永不改变的列或不需要历史logging的列,以免臃肿。 即使可以从旧值和新值导出,为数值存储增量可以使后续查询更容易。
历史logging表必须安全,防止非系统用户插入,更新或删除行。 应该只支持定期清除,以减less整体大小(如果用例允许的话)。
我们已经实施了一个非常类似克里斯·罗伯茨(Chris Roberts)所提出的解决scheme的解决scheme,而且这对我们来说工作得非常好
唯一的区别是我们只存储新值。 旧的值毕竟存储在前一个历史logging行中
[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列的表。 这样,您只需要存储已更改的确切列,而不必存储整行。
如果您必须存储历史logging,请使用与您正在追踪的表格相同的模式以及“修订date”和“修订types”列(例如“删除”,“更新”)创build影子表。 编写(或生成 – 见下文)一组触发器来填充审计表。
制作一个能够读取表的系统数据字典的工具并生成一个创build影子表的脚本以及一组触发器来填充它,这相当简单。
不要试图为此使用XML,XML存储比这种types的触发器使用的本地数据库表存储效率低很多。
避免devise1; 一旦你需要回滚到旧版本的logging – 使用pipe理员控制台自动或“手动”,它不是非常方便。
我没有看到Design 2的缺点。我认为第二个,History表应该包含第一个Records表中的所有列。 例如,在MySQL中,您可以轻松创build与另一个表具有相同结构的表( create table X like Y
)。 而且,如果您要更改实时数据库中Records表的结构,则无论如何都必须使用alter table
命令 – 而且对于您的历史logging表也没有太大的努力来运行这些命令。
笔记
- logging表只包含最新版本;
- 历史logging表包含logging表中所有logging的所有修订;
- 历史logging表的主键是添加了
RevisionId
列的logging表的主键; - 考虑像
ModifiedBy
这样的附加字段 – 创build特定版本的用户。 您可能还希望有一个字段DeletedBy
来跟踪删除特定修订的人员。 - 想想
DateModified
应该是什么意思 – 要么意味着创build这个特定版本的位置,要么意味着这个特定版本被另一个版本替代。 前者要求该领域在logging表中,一见钟情似乎更直观; 第二种解决办法似乎对删除的logging更为实际(删除此特定修订的date)。 如果你select第一个解决scheme,你可能需要第二个字段DateDeleted
(只有当你需要的时候)。 取决于你和你真正想要logging的东西。
devise2中的操作非常简单:
修改
- 将Records表中的logging复制到History表中,给它一个新的RevisionId(如果它不在Records表中),则处理DateModified(取决于你如何解释它,见上面的注释)
- 继续正常更新logging表中的logging
删除
- 与“修改”操作的第一步完全相同。 根据您select的解释,相应地处理DateModified / DateDeleted。
取消删除(或回滚)
- 从历史logging表中取最高(或某个特定的?)修订,并将其复制到logging表
列出特定logging的修订历史
- 从历史表和logging表中select
- 想想你对这个行动的期望是什么; 它可能会确定您需要从DateModified / DateDeleted字段中获取哪些信息(请参阅上面的注释)
如果您使用Design 2,那么所有需要执行的SQL命令都将非常简单,并且可以进行维护! 也许, 如果您在Records表中也使用辅助列( RevisionId
, DateModified
) ,将会更容易– 使两个表保持完全相同的结构 (唯一键除外)! 这将允许简单的SQL命令,这将容忍任何数据结构的变化:
insert into EmployeeHistory select * from Employe where ID = XX
不要忘记使用交易!
至于缩放 ,这个解决scheme是非常高效的,因为您不需要从XML中来回转换任何数据,只需要复制整个表格行 – 非常简单的使用索引的查询 – 非常高效!
Ramesh,我参与了基于第一种方法的系统开发。
事实certificate,将修订保存为XML将导致数据库巨大的增长,并显着减慢速度。
我的做法是每个实体一个表格:
Employee (Id, Name, ... , IsActive)
IsActive是最新版本的标志
如果您想将某些附加信息与修订相关联,则可以创build包含该信息的单独表格,并使用PK \ FK关系将其与实体表格链接起来。
这样,您可以将所有版本的员工存储在一个表中。 这种方法的优点:
- 简单的数据库结构
- 由于表变成仅追加,所以没有冲突
- 只需更改IsActive标志即可回滚到以前的版本
- 不需要连接来获取对象历史logging
请注意,您应该允许主键不唯一。
我以前见过的这种做法是有的
Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );
你从来没有“更新”这个表(除了改变isCurrent的有效性),只需插入新的行。 对于任何给定的EmployeeId,只有一行可以有isCurrent == 1。
维护它的复杂性可以通过视图和“而不是”触发器来隐藏(在oracle中,我假设其他RDBMS也是类似的),如果表太大而不能被索引处理,甚至可以去实现视图) 。
这种方法是可以的,但是你可以结束一些复杂的查询。
就我个人而言,我非常喜欢你的devise2的做法,这也是我过去的做法。 它简单易懂,实施简单,维护简单。
它也为数据库和应用程序创造了很less的开销,特别是在执行读取查询时,这可能是99%的时间。
自动创build历史logging表和触发器以维护(假设通过触发器完成)也是相当容易的。
如果确实需要审计跟踪,我会倾向于审计表解决scheme(在其他表上重要列的非规范化副本完成,例如UserName
)。 但请记住,这种痛苦的经历表明,单一的审计表将成为一个巨大的瓶颈; 可能值得为所有审计表创build单独的审计表。
如果您需要跟踪实际的历史(和/或未来)版本,那么标准解决scheme是使用开始,结束和持续时间值的一些组合跟踪具有多行的相同实体。 您可以使用视图来方便地访问当前值。 如果这是您采取的方法,如果您的版本化数据引用可变但未版本化的数据,则可能会遇到问题。
数据的修改是时间数据库“ 有效时间 ”概念的一个方面。 许多研究已经涉及到这一点,许多模式和指导方针已经出现。 对于那些感兴趣的人,我写了一大堆对这个问题的引用。
如果你想做第一个,你可能也想为Employees表使用XML。 大多数较新的数据库允许您查询XML字段,所以这并不总是一个问题。 无论是最新版本还是早期版本,使用单向访问员工数据的方式可能会更简单。
我会尝试第二种方法。 您可以通过只有一个带有DateModified字段的Employees表来简化此操作。 EmployeeId + DateModified将是主键,您可以通过添加一行来存储新版本。 这种归档旧版本和从归档恢复版本也更容易。
另一种做法可能是Dan Linstedt的datavault模型 。 我为荷兰统计局做了一个使用这个模型的项目,效果很好。 但是我不认为这对于日常的数据库使用是直接有用的。 你可能会从阅读他的论文中得到一些想法。
我将与您分享我的devise,这与您的两种devise不同,因为它需要每种实体types一张桌子。 我发现描述任何数据库devise的最好方法是通过ERD,这里是我的:
在这个例子中,我们有一个名为employee的实体。 用户表保存你的用户的logging和实体,而entity_revision是两个表,它们保存你系统中所有实体types的修订历史logging。 以下是这个devise的工作原理:
entity_id和revision_id的两个字段
系统中的每个实体都将拥有自己的唯一实体标识。 您的实体可能会经历修改,但其entity_id将保持不变。 您需要在您的员工表中保留此实体ID(作为外键)。 您还应该将实体的types存储在实体表(例如“员工”)中。 现在至于revision_id,正如其名称所示,它跟踪您的实体修订版本。 我发现的最好办法是使用employee_id作为你的revision_id。 这意味着你将有不同types的实体重复的修订标识符,但这不是对待我(我不知道你的情况)。 唯一需要注意的是entity_id和revision_id的组合应该是唯一的。
entity_revision表中还有一个状态字段,表示修改的状态。 它可以有三个状态之一: latest
, obsolete
或deleted
(不依赖于修订date可以帮助你很大的提高你的查询)。
关于revision_id的最后一个注释,我没有创build一个将employee_id连接到revision_id的外键,因为我们不希望为将来可能添加的每个实体types更改entity_revision表。
INSERTION
对于要插入到数据库中的每个员工 ,还将为实体和实体参数添加logging。 最后两条logging将帮助您跟踪logging被插入到数据库的人员和时间。
UPDATE
现有员工logging的每个更新将以两个插入实现,一个位于员工表中,另一个位于entity_revision中。 第二个将帮助你知道logging被更新的人和时间。
缺失
为了删除雇员,logging被插入到entity_revision中,表明删除和完成。
正如你在这个devise中看到的,从数据库中不会更改或删除数据,更重要的是每个实体types只需要一个表。 我个人觉得这个devise非常灵活,易于使用。 但是我不确定你的需求可能会有所不同。
[UPDATE]
在新的MySQL版本中支持分区后,我相信我的devise也带有最好的性能之一。 可以使用type
字段对entity
表进行分区, entity_revision
使用其state
字段对entity_revision
进行分区。 这样可以大大提高SELECT
查询的效率,同时保持devise的简洁性。
怎么样:
- 员工ID
- DateModified
- 和/或版本号,具体取决于您如何跟踪它
- ModifiedByUSerId
- 加上你想跟踪的任何其他信息
- 员工领域
您创build主键(EmployeeId,DateModified),并为每个employeeidselectMAX(DateModified)以获取“当前”logging。 存储一个IsCurrent是一个非常糟糕的主意,因为首先可以计算出来,其次,数据很容易失去同步。
您也可以制作一个仅列出最新logging的视图,并且大多数情况下在您的应用中使用该logging。 这种方法的好处是你没有重复的数据,你不必从两个不同的地方(当前在雇员,并存档在EmployeesHistory)来获取所有的历史或回滚等数据) 。
如果你想依靠历史数据(为了报告的原因),你应该使用这样的结构:
// Holds Employee Entity "Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)" // Holds the Employee revisions in rows. "EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"
或者全球应用解决scheme:
// Holds Employee Entity "Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)" // Holds all entities revisions in rows. "EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"
您也可以将修订保存为XML格式,然后您只有一个修订logging。 这将看起来像:
// Holds Employee Entity "Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)" // Holds all entities revisions in rows. "EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"
我们有类似的要求,我们发现,用户往往只是想看看有什么变化,而不一定回滚任何变化。
我不确定你的用例是什么,但是我们所做的就是创build和审计表,它随着业务实体的变化而自动更新,包括任何外键引用和枚举的友好名称。
每当用户保存他们的变化,我们重新加载旧的对象,运行一个比较,logging更改,并保存实体(全部在单个数据库事务中完成,以防有任何问题)。
这似乎对我们的用户非常有效,并且为我们节省了与业务实体具有相同字段的完全独立审计表的麻烦。
这听起来像是你想跟踪对特定实体随时间的变化,例如ID 3,“bob”,“123 main street”,然后是另一个ID 3,“bob”,“234 elm st”等等,实质上能够呕吐修订历史显示每个地址“鲍勃”已经在。
要做到这一点的最佳方法是在每个logging上都有一个“当前”字段,并且(可能)将一个时间戳或FKlogging到date/时间表中。
插入必须然后设置“是当前的”,并且还取消在先前的“当前”logging上的“当前”。 查询必须指定“是当前的”,除非你想要所有的历史。
如果它是一个非常大的表格,或者预计会有大量的修订,那么还有更多的调整,但这是一个相当标准的方法。