假设我在数据库中有一条记录,管理员和普通用户都可以进行更新。
任何人都可以建议一个好的方法/架构如何版本控制此表中的每个更改,以便可以将记录回滚到以前的版本。
答案 0 :(得分:149)
假设您有一个FOO
表,管理员和用户可以更新。大多数情况下,您可以针对FOO表编写查询。快乐的日子。
然后,我会创建一个FOO_HISTORY
表。这包含FOO
表的所有列。主键与FOO和RevisionNumber列相同。有一个从FOO_HISTORY
到FOO
的外键。您还可以添加与修订相关的列,例如UserId和RevisionDate。在所有*_HISTORY
表中(即从Oracle序列或等效表)中以不断增加的方式填充RevisionNumbers。不要只依赖于那里只有一次更改(即不要将RevisionDate
放入主键)。
现在,每次更新FOO
时,就在您进行更新之前,将旧值插入FOO_HISTORY
。您可以在设计的某个基础级别执行此操作,以便程序员不会意外地错过此步骤。
如果您想从FOO
删除一行,您有一些选择。级联并删除所有历史记录,或通过将FOO
标记为已删除来执行逻辑删除。
当您对目前的价值感兴趣并且仅偶尔出现在历史中时,此解决方案很好。如果您总是需要历史记录,那么您可以设置有效的开始和结束日期,并将所有记录保存在FOO
本身。然后每个查询都需要检查这些日期。
答案 1 :(得分:40)
我认为您正在寻找版本化数据库记录的内容(正如StackOverflow在有人编辑问题/答案时所做的那样)。一个好的起点可能是查看一些使用 revision 跟踪的数据库模型。
想到的最好的例子是维基百科引擎MediaWiki。比较数据库图表here,尤其是revision table。
根据您使用的技术,您必须找到一些好的差异/合并算法。
检查this question是否适用于.NET。
答案 2 :(得分:25)
在BI世界中,您可以通过向要版本的表添加startDate和endDate来实现此目的。将第一条记录插入表中时,将填充startDate,但endDate为null。插入第二条记录时,还会使用第二条记录的startDate更新第一条记录的endDate。
如果要查看当前记录,请选择endDate为空的记录。
这有时称为类型2 Slowly Changing Dimension。 另请参阅TupleVersioning
答案 3 :(得分:8)
升级到SQL 2008。
尝试在SQL 2008中使用SQL更改跟踪。您可以使用此新功能跟踪数据库中数据的更改,而不是时间戳和逻辑删除列黑客攻击。
答案 4 :(得分:5)
两个选项:
答案 5 :(得分:5)
只是想补充说这个问题的一个好方法是使用Temporal database。许多数据库供应商都提供开箱即用或扩展的功能。我已成功使用PostgreSQL的temporal table扩展名,但其他人也使用了它。每当更新数据库中的记录时,数据库也会保留该记录的先前版本。
答案 6 :(得分:3)
你没有说什么数据库,我没有在post标签中看到它。如果是Oracle的话,我可以推荐Designer中内置的方法:使用journal tables。如果它适用于任何其他数据库,那么我基本上也推荐相同的方式......
它的工作方式,如果你想在另一个数据库中复制它,或者如果你只想了解它,那么对于一个表,也会创建一个影子表,只是一个普通的数据库表,相同的字段规范,加上一些额外的字段:比如上次采取的操作(字符串,插入的典型值“INS”,更新的“UPD”和删除的“DEL”),操作发生时的日期时间以及用户ID谁做了。
通过触发器,对表中任何行的每个操作都会在日志表中插入一个新行,其中包含新值,采取的操作,时间和用户。您永远不会删除任何行(至少在过去几个月内没有删除)。是的,它会变大,容易上百万行,但是您可以轻松跟踪任何时间点任何记录的值自日记记录开始或旧日记帐行最后一次被清除,谁做了最后的改变。
在Oracle中,您需要的所有内容都是作为SQL代码自动生成的,您所要做的就是编译/运行它;它带有一个基本的CRUD应用程序(实际上只有“R”)来检查它。
答案 7 :(得分:3)
您可以通过SQL触发器对SQL表执行审核。从触发器中,您可以访问2个特殊表(inserted and deleted)。这些表包含每次更新表时插入或删除的确切行。在触发器SQL中,您可以获取这些已修改的行并将它们插入到审计表中。这种方法意味着您的审计对程序员来说是透明的;不需要他们的努力或任何实施知识。
这种方法的另一个好处是,无论sql操作是通过数据访问DLL还是通过手动SQL查询进行,都会发生审计; (因为审核是在服务器上进行的)。
答案 8 :(得分:2)
我也在做同样的事情。我正在为课程计划建立一个数据库。这些计划需要原子更改版本控制灵活性。换句话说,每次改变,无论多小,都需要允许,但旧版本也需要保持完整。这样,课程创建者可以在学生使用课程计划时编辑课程计划。
它的工作方式是,一旦学生完成课程,他们的结果将附在他们完成的版本上。如果进行了更改,其结果将始终指向其版本。
这样,如果删除或移动课程标准,其结果将不会更改。
我目前这样做的方法是处理一个表中的所有数据。通常我会有一个id字段,但是使用这个系统,我使用的是id和sub_id。通过更新和删除,sub_id始终与行保持一致。 id自动递增。课程计划软件将链接到最新的sub_id。学生结果将链接到id。我还提供了一个时间戳,用于跟踪发生更改的时间,但没有必要处理版本控制。
我可能会改变一件事,一旦我测试了它,我可能会使用前面提到的endDate null想法。在我的系统中,要找到最新版本,我必须找到max(id)。另一个系统只查找endDate = null。不确定好处是否有另一个日期字段。
我的两分钱。
答案 9 :(得分:2)
而@WW。答案是一个很好的答案另一种方法是制作一个版本列并将所有版本保存在同一个表中。
对于一个表格方法,您可以:
outer join
更令人讨厌的东西。使用修订号的outer join
方法的示例SQL是:
SELECT tc.*
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL
AND tc.path = '/stuff' -- path in this case is our natural id.
坏消息是上面要求outer join
,外连接可能很慢。好消息是创建新条目在理论上更便宜,因为你可以在一次没有事务的写操作(假设你的数据库是原子的)中这样做。
为'/stuff'
制作新版本的示例可能是:
INSERT INTO text_content (id, path, data, revision, revision_comment, enabled, create_time, update_time)
(
SELECT
(md5(random()::text)) -- {id}
, tc.path
, 'NEW' -- {data}
, (tc.revision + 1)
, 'UPDATE' -- {comment}
, 't' -- {enabled}
, tc.create_time
, now()
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL
AND tc.path = '/stuff' -- {path}
)
我们使用旧数据插入。如果您只想更新一列并避免乐观锁定和/或事务,这将特别有用。
标记方法和历史记录表方法需要插入/更新两个行。
outer join
版本号方法的另一个优点是,您可以随后使用触发器重构多表方法,因为您的触发器应该基本上执行上述操作。
答案 10 :(得分:2)
Alok在上面的Audit table
中建议,我想在我的帖子中进行解释。
我在项目中采用了这种无模式的单表设计。
架构:
此表可以一次保存每个表的历史记录,而在一条记录中包含完整的对象历史记录。该表可以使用触发器/挂钩来填充,其中数据发生更改,并存储目标行的新旧值快照。
具有这种设计的专业人员:
与此设计有关:
答案 11 :(得分:0)
作为对我上面答案的附加步骤,我建议为每个生成的更改提供唯一的ID,可能带有日期/时间和每天的唯一计数器(这样一来,多次更新就不会重叠) 。我将在该代码中包含一个动作类型代码,因此为“ 9129128213939REPLACE”。这提供了一种鲁棒性,以允许您明智地检查您的其他历史记录系统是否正常工作。