实现数据库对象的版本控制

5
我即将开始一个项目,从规格上看有点像StackOverflow。基本上,这是一个具有用户控制内容的Web应用程序。
其中一个让我困惑不已的功能是版本控制。在StackOverflow上,每个问题和答案都可以有多个修订版本。当你只有一种类型的对象(在这种情况下是文本)时,这很容易实现。
所以,对于我的简单页面,我已经设置好了。
但是,当我考虑需要进行版本控制的某些对象具有关系时,问题就出现了。为了提供一个具体的例子,让我们选择一个随机类似的领域:
假设我正在实施一个类似Wiki的站点来跟踪书籍/作者信息。该站点的主要重点将是创建和更新“作者”页面,这在文本方面非常简单(如上所述)。但是,让我们在作者和书籍之间添加一个一对多的关联(换句话说,书籍将是单独的对象,因为一个人可以撰写多本书)。每本书都将从作者页面链接到有关该书的信息页面。
对于用户而言,在描述作者的基于文本的“摘要”和作者与其作品之间的链接之间几乎没有区别。因此,我们必须实现作者页面、图书页面和作者与书籍之间的“修订”/编辑功能。换句话说,用户应该能够编辑、查看作者页面、书籍页面和两者之间的关联历史记录,并回滚。
当这种关系变成多对多时,情况会变得更加复杂,因为多个作者可能被列为为一本书做出贡献的作者。
我有许多解决方案,但没有一个像我想要的那样干净(并涉及至少一些重复代码/冗余数据存储),尽管我确实在这里看到了共性,但我感觉我还没有真正能够最好地提取它,特别是在数据库层面上。我不想影响给出的答案,所以我不会立即给出答案。
那么,在数据库级别上,你会如何设计这个系统?我在这里寻找表规格,如果不是显而易见的话,可能还需要描述如何使用它们。对于那些可能相关的答案,我将使用ASP.NET和Linq-to-SQL(我熟悉LTS中的多对多)或Entity Framework。
编辑:澄清一下,我了解基本的DB设计、标准化、多对多映射表等。我正在寻找这种具体情况的简洁解决方案。
编辑2:我正在寻找通用解决方案,因为系统中可能会有比书籍更多的子对象。作者可能与其他作者、杂志、事件等相关联,我感觉如果我为每个人单独实现历史记录,我就要重复很多工作。

@JoshJordan:不要道歉。 修正问题,让它更加简洁明了。 - S.Lott
3个回答

5
这是数据仓库中常见的问题。它们使用“缓慢变化的维度”。
然而,如果您要尝试拥有“版本化”的数据,就必须有一些规则。
1.必须记录作者-书籍关系,如最初定义的那样。这是官方的作者-书籍关系。这是数据仓库专家所称的“无事实表”。它是一对键。
2.书是书籍作者事实的一个维度。书本可能会有变化。有许多缓慢变化的维度算法。您可以仅保留最新版本,将历史表与当前表分开。在一个表中保留历史和当前数据,并用标记区分当前数据和历史数据。
3.作者是书籍作者事实的一个维度。作者可能会发生变化。同样,有许多SCD算法可供选择。阅读有关选择的Ralph Kimball的The Data Warehouse Toolkit以获取更多信息。
请注意,关系(作者到书籍)是一个事实,不需要版本。它是一个事实。它不会“变化”。它要么是真实的,要么是错误地放入数据库中——在这种情况下,它必须被删除。事实不需要版本号。
在更复杂的星型模式中,您的事实具有措施。价格、销售量、成本、利润等。这些信息也记录在事实表中。这些信息随时间变化。因此,每个事实几乎总是有一个时间维度。
因此,时间是书籍作者事实的一个维度。如果这个事实可以更改,适用的时间段将作为事实的一部分记录下来。
时间维度与版本号不完全相同。它比较简单。它说明在某个特定的时间点,事实是正确的。如果事实发生变化,您可以附加一个具有不同时间戳的新事实。
在特定的时间点,您可以找到相关的事实和关联的维度值。

谢谢。我应该重新考虑为什么/是否要为每个表单单独创建一个历史记录表,而不是将每个表单的旧数据保存在表单本身中。 - ChrisW
@ChrisW:SCD 设计很难。这取决于您将得到的查询类型。人们是否进行“反事实”(“假设”)查询?“如果去年的地区定义报告了这些销售数字,会怎样?”在这种情况下,您可能会加入历史维度行。如果您很少这样做,则单独的历史记录表不会有太大影响。如果您经常这样做,则单独的历史记录表可能过于复杂。 - S.Lott
一个问题是,在同一张表中拥有历史数据会使实现引用完整性更加困难(或不太直观)。例如,可能存在每本书都有对应作者的约束要求。通常,您可以使用外键来实现此目的。但是,如果作者表包含已删除的作者,则外键不是足够的约束条件(因为它将允许您插入一个新书,该书引用了已删除的作者)。 - ChrisW
不是简单的外键,我猜你需要将其定义为(更复杂的)CHECK约束,该约束应检查具有给定AuthorId但也具有所需AuthorStatus值的记录。 - ChrisW

1
我有两张表,分别是作者(Author)和书籍(Book)。
这两个表之间有通常的外键关系(无论是什么)。
每个表还有一个历史记录表,即AuthorHistory和BookHistory。这些历史记录表包含记录的旧/过时版本(例如,每个已删除或编辑的作者记录)。历史记录表之间没有外键关系。

编辑:

每个表的某些功能都是相似的:例如,无论哪个表格,更新记录意味着将旧记录的副本存储在相应的历史记录表中。我使用数据库触发器(每个表的更新和删除触发器)来实现此功能;因为我使用的数据库引擎支持触发器,这使得它对应用程序透明。这些触发器中的代码从一个表到另一个表是相似的(只有表的名称和字段名称列表与其他表不同)。


关于多对多的情况呢?这更加困难,因为你实际上可能没有记录将作者映射到一本书上,但之前可能有过,并且需要将其显示为历史项。
编辑#2:
我还没有实现多对多情况的历史记录,但我不认为它会有什么不同,即:
- 多对多关系是通过存在一个BookAuthor表来实现的,每个记录只是BookId加上AuthorId。 - 历史关系在相应的BookAuthorHistory表中。

那么多对多的情况呢?这更加困难,因为您实际上可能没有记录将作者映射到书籍,但之前可能有过一条记录,并且需要将其显示为历史项。 - JoshJordan
确实,你是对的。不幸的是,这不是一个非常通用/可扩展的解决方案。它需要为每个新实现的表创建一个新的历史记录表。 - JoshJordan
我不明白这个方案有什么不普适/可扩展的地方:在我看来,它是一个“通用”的解决方案,因为它适用于任何一组表。 - ChrisW
从设计角度来看,我正在寻找一种通用的实现。 - JoshJordan
我不知道你的意思。无论如何,完全相反的另一种解决方案是拥有一个单独的历史记录表,其中包含数据库中每个其他表中的所有历史字段值:http://en.wikipedia.org/wiki/Entity-attribute-value_model(我看到有人说使用EAV是程序员在职业生涯中犯的错误之一)。 - ChrisW

1

这似乎是使用CouchDB的理想用例。使用这种面向文档的数据库,您可以免费获得修订记录(每个文档都会自动进行修订,除非您配置数据库不同)。

还可以在文档之间建立m:n关系。但是,迁移到CouchDB是一个相当大的步骤,我不知道它在ASP.NET中的可访问性如何。但阅读一些入门教程不会有坏处。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接