如何对数据库中的记录进行版本控制


176

假设我在数据库中有一条记录,并且管理员和普通用户都可以进行更新。

任何人都可以提出一个好的方法/体系结构来控制该表中的每个更改的版本,从而可以将记录回滚到以前的修订版。

Answers:


164

假设您有一个FOO可供管理员和用户更新的表。大多数时候,您可以针对FOO表编写查询。快乐的时光。

然后,我将创建一个FOO_HISTORY表。这具有表的所有列FOO。主键与FOO相同,外加一个RevisionNumber列。有一个从FOO_HISTORY到的外键FOO。您可能还会添加与修订有关的列,例如UserId和RevisionDate。以越来越多的方式在所有*_HISTORY表中填充RevisionNumbers (即从Oracle序列或等效表中)。不要只依赖在一秒钟内进行一次更改(即不要放入RevisionDate主键中)。

现在,每次更新时FOO,就在您进行更新之前,将旧值插入FOO_HISTORY。您需要在设计的某个基本级别上执行此操作,以使程序员不会意外遗漏此步骤。

如果要从中删除行,则FOO有一些选择。级联并删除所有历史记录,或者通过将其标记FOO为已删除来执行逻辑删除。

当您对当前值非常感兴趣并且仅对历史记录感兴趣时,此解决方案是不错的选择。如果您始终需要历史记录,则可以输入有效的开始日期和结束日期,并将所有记录保留在FOO其内。然后,每个查询都需要检查这些日期。


1
如果您的数据访问层不直接支持审计触发器,则可以使用数据库触发器进行审计表更新。同样,构建代码生成器以使用系统数据字典中的自省功能的触发器并不难。
ConcernedOfTunbridgeWells,

44
我建议您实际插入数据,而不是以前的数据,因此历史记录表包含所有数据。尽管它存储redyundent数据,但它消除了需要历史数据时在两个表中进行搜索所需的特殊情况。
Nerdfest

6
我个人建议不要删除任何内容(将其推迟到特定的内部管理活动中),并使用“操作类型”列来指定它是插入/更新/删除。对于删除,您可以照常复制该行,但在操作类型列中输入“删除”。
尼尔·巴恩韦尔

3
@Hydrargyrum包含当前值的表的性能将优于历史表的视图。您可能还想定义引用当前值的外键。
WW。

2
There is a foreign key from FOO_HISTORY to FOO':不好的主意,我想从foo中删除记录而不更改历史记录。在正常使用情况下,历史记录表应仅插入。
杰森2015年

46

我认为您正在寻找数据库记录内容的版本控制(就像有人编辑问题/答案时StackOverflow所做的那样)。一个好的起点可能是查看一些使用修订跟踪的数据库模型。

想到的最好的例子是Wikipedia引擎MediaWiki。比较这里的数据库图,尤其是修订表

根据您使用的技术,您必须找到一些好的差异/合并算法。

检查此问题是否适用于.NET。


30

在BI世界中,您可以通过将startDate和endDate添加到要版本化的表中来实现此目的。当您将第一条记录插入表中时,将填充startDate,但endDate为null。当插入第二条记录时,您还用第二条记录的startDate更新了第一条记录的endDate。

当您要查看当前记录时,请选择endDate为null的记录。

有时称为2型缓慢变化尺寸。另请参见TupleVersioning


使用这种方法,我的桌子不会变得很大吗?
尼尔斯·波斯玛

1
是的,但是您可以通过对表进行索引和/或分区来解决该问题。同样,只有少数几张大桌子。大多数将更小。
ConcernedOfTunbridgeWells

如果我没记错的话,这里唯一的缺点就是它将更改限制为每秒一次正确吗?
pimbrouwers

@pimbrouwers是的,它最终取决于字段的精度和填充字段的功能。
Dave Neeley

9

升级到SQL 2008。

尝试在SQL 2008中使用SQL更改跟踪。您可以使用此新功能来跟踪数据库中数据的更改,而无需使用时间戳和逻辑删除列hack。

MSDN SQL 2008更改跟踪


7

只是想添加一个解决此问题的好方法是使用Temporal数据库。许多数据库供应商开箱即用或通过扩展提供了此功能。我已经在PostgreSQL中成功使用了时态表扩展,但是其他人也有。每当您更新数据库中的记录时,数据库也会保留该记录的先前版本。


6

两种选择:

  1. 有一个历史记录表-每当原始数据更新时,将旧数据插入此历史记录表。
  2. 审核表-存储之前和之后的值-仅用于审核表中修改后的列,以及其他信息(如谁更新和何时更新)。

5

您可以通过SQL触发器对SQL表执行审核。通过触发器,您可以访问2个特殊表(已插入和已删除)。这些表包含每次更新表时插入或删除的确切行。在触发器SQL中,您可以将这些修改后的行插入到审计表中。这种方法意味着您的审核对程序员是透明的。不需要他们的努力或任何实施知识。

这种方法的额外好处是,无论是通过数据访问DLL还是通过手动SQL查询进行sql操作,都将进行审核。(因为审核是在服务器本身上执行的)。


3

您没有说什么数据库,我也没有在post标签中看到它。如果是针对Oracle的,我可以推荐Designer中内置的方法:使用日记表。如果是用于其他任何数据库,那么我基本上也建议采用相同的方式...

如果您想在另一个数据库中复制它,或者如果您只是想了解它,那么它的工作方式是对于一个表也创建了一个影子表,只是一个普通的数据库表,具有相同的字段规格,以及一些其他字段:例如上次执行的操作(字符串,典型值“ INS”表示插入,“ UPD”表示更新和“ DEL”表示删除),执行操作的日期时间以及执行操作的用户ID它。

通过触发器,对表中任何行的每个操作都会在日记表中插入一个新行,其中包含新值,执行了什么操作,何时以及由哪个用户执行。您永远不会删除任何行(至少最近几个月不会删除)。是的,它会变得很大,可以轻松地行达数百万行,但是您可以轻松地自日志记录开始或旧的日志行被最后清除以及谁进行了最后更改以来的任何时间跟踪任何记录的值。

在Oracle中,您需要的所有内容都将作为SQL代码自动生成,您所要做的就是编译/运行它。它带有一个基本的CRUD应用程序(实际上只有“ R”)来检查它。


2

我也在做同样的事情。我正在为课程计划建立数据库。这些计划需要原子更改版本控制灵活性。换句话说,对课程计划的每次更改(无论大小)都必须允许,但旧版本也必须保持完整。这样,课程创建者可以在学生使用课程计划时对其进行编辑。

它的工作方式是,一旦学生完成一堂课,他们的成绩就会附在他们完成的版本上。如果进行更改,其结果将始终指向其版本。

这样,如果删除或移动课程标准,其结果将不会更改。

我目前这样做的方式是通过在一个表中处理所有数据。通常,我只有一个id字段,但是在这个系统中,我使用的是id和sub_id。通过更新和删除,sub_id始终与该行保持一致。ID是自动递增的。课程计划软件将链接到最新的sub_id。学生成绩将链接到ID。我还提供了一个时间戳,用于跟踪更改发生的时间,但是没有必要处理版本控制。

经过测试,我可能会改变的一件事是,我可能会使用前面提到的endDate null想法。在我的系统中,要找到最新版本,我必须找到max(id)。另一个系统只是寻找endDate = null。不知道收益是否超出了另一个日期字段。

我的两分钱。


2

而@WW。答案是一个好答案。另一种方法是创建一个版本列,并将所有版本保留在同一表中。

对于一个表方法,您要么:

  • 使用标记来指示最新的ala Word Press
  • 或者做一个比版本更大的讨厌的东西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修订号方法的另一个优点是,您以后总是可以使用触发器来重构为多表方法,因为触发器本质上应该执行上述操作。


2

阿洛克建议 Audit table在上面,我想在我的帖子中进行解释。

我在项目中采用了这种无模式的单表设计。

架构:

  • id-整数自动递增
  • 用户名-STRING
  • 表名-STRING
  • oldvalue-TEXT / JSON
  • newvalue-TEXT / JSON
  • createdon-DATETIME

该表可以一次保存所有表的历史记录,而一条记录中包含完整的对象历史记录。该表可以使用触发器/挂钩来填充,数据在触发器/挂钩中更改,并存储目标行的新旧值快照。

具有这种设计的优点:

  • 用于历史记录管理的表数量更少。
  • 存储每行新旧状态的完整快照。
  • 易于搜索每个表。
  • 可以按表创建分区。
  • 可以为每个表定义数据保留策略。

与此设计的缺点:

  • 如果系统频繁更改,则数据大小可能会很大。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.