数据库应用程序中一个相当普遍的要求是跟踪数据库中一个或多个特定实体的更改。我听说这称为行版本控制,日志表或历史表(我确定还有其他名称)。在RDBMS中有许多方法可以处理它 - 您可以将所有源表中的所有更改写入单个表(更多日志),或者为每个源表创建单独的历史表。您还可以选择管理应用程序代码中的日志记录或通过数据库触发器。
我正在尝试思考在NoSQL /文档数据库(特别是MongoDB)中对同一问题的解决方案是什么样的,以及如何以统一的方式解决它。它是否像为文档创建版本号一样简单,从不覆盖它们?为“真实”与“已记录”文档创建单独的集合?这将如何影响查询和性能?
无论如何,这是否是NoSQL数据库的常见情况,如果有,是否有共同的解决方案?
答案 0 :(得分:91)
好问题,我自己也在研究这个问题。
我遇到了用于Ruby的Mongoid驱动程序的Versioning module。我自己没有使用它,但是从what I could find开始,它为每个文档添加了一个版本号。旧版本嵌入在文档中。主要缺点是整个文档在每次更改时都会重复,这将导致在处理大型文档时存储大量重复内容。这种方法很好,但是当你处理小型文档和/或不经常更新文档时。
另一种方法是仅存储新版本中已更改的字段。然后,您可以“展平”您的历史记录以重建文档的任何版本。但这非常复杂,因为您需要跟踪模型中的更改并以应用程序可以重建最新文档的方式存储更新和删除。这可能很棘手,因为您正在处理结构化文档而不是平面SQL表。
每个字段也可以有单独的历史记录。通过这种方式将文档重建为给定版本更加容易。在您的应用程序中,您不必显式跟踪更改,而只需在更改其值时创建属性的新版本。文档看起来像这样:
{
_id: "4c6b9456f61f000000007ba6"
title: [
{ version: 1, value: "Hello world" },
{ version: 6, value: "Foo" }
],
body: [
{ version: 1, value: "Is this thing on?" },
{ version: 2, value: "What should I write?" },
{ version: 6, value: "This is the new body" }
],
tags: [
{ version: 1, value: [ "test", "trivial" ] },
{ version: 6, value: [ "foo", "test" ] }
],
comments: [
{
author: "joe", // Unversioned field
body: [
{ version: 3, value: "Something cool" }
]
},
{
author: "xxx",
body: [
{ version: 4, value: "Spam" },
{ version: 5, deleted: true }
]
},
{
author: "jim",
body: [
{ version: 7, value: "Not bad" },
{ version: 8, value: "Not bad at all" }
]
}
]
}
在某个版本中将部分文档标记为已删除但仍然有点尴尬。您可以为可以从应用程序中删除/恢复的部分引入state
字段:
{
author: "xxx",
body: [
{ version: 4, value: "Spam" }
],
state: [
{ version: 4, deleted: false },
{ version: 5, deleted: true }
]
}
使用这些方法中的每一种,您都可以在一个集合中存储最新的扁平版本,并将历史数据存储在单独的集合中。如果您只对文档的最新版本感兴趣,这应该可以缩短查询时间。但是当您需要最新版本和历史数据时,您需要执行两个查询,而不是一个。因此,使用单个集合与两个单独集合的选择应取决于您的应用程序需要历史版本的频率。
这个答案的大部分内容只是我思想的大脑转移,我还没有尝试过任何这个问题。回顾它,第一个选项可能是最简单和最好的解决方案,除非重复数据的开销对您的应用程序非常重要。第二种选择非常复杂,可能不值得努力。第三种选择基本上是选项二的优化,应该更容易实现,但可能不值得实施,除非你真的不能选择第一种。
期待对此的反馈,以及其他人对该问题的解决方案:)
答案 1 :(得分:7)
我们已经在我们的网站上部分实现了这一点,并且我们使用“在单独的文档中存储修订”(和单独的数据库)。我们编写了一个自定义函数来返回差异,我们存储它。不是很难并且可以允许自动恢复。
答案 2 :(得分:4)
为什么在文档中存储更改的变体?
不是针对每个密钥对存储版本,而是文档中的当前密钥对始终表示最新状态,并且变更的“日志”存储在历史数组中。只有那些自创建以来已更改的密钥才会在日志中有一个条目。
{
_id: "4c6b9456f61f000000007ba6"
title: "Bar",
body: "Is this thing on?",
tags: [ "test", "trivial" ],
comments: [
{ key: 1, author: "joe", body: "Something cool" },
{ key: 2, author: "xxx", body: "Spam", deleted: true },
{ key: 3, author: "jim", body: "Not bad at all" }
],
history: [
{
who: "joe",
when: 20160101,
what: { title: "Foo", body: "What should I write?" }
},
{
who: "jim",
when: 20160105,
what: { tags: ["test", "test2"], comments: { key: 3, body: "Not baaad at all" }
}
]
}
答案 3 :(得分:2)
可以拥有当前的NoSQL数据库和历史NoSQL数据库。每天都会有一个夜间ETL。此ETL将使用时间戳记录每个值,因此它将始终是元组(版本化字段)而不是值。如果对当前值进行了更改,它将仅记录新值,从而节省了流程中的空间。例如,这个历史NoSQL数据库json文件可能如下所示:
{
_id: "4c6b9456f61f000000007ba6"
title: [
{ date: 20160101, value: "Hello world" },
{ date: 20160202, value: "Foo" }
],
body: [
{ date: 20160101, value: "Is this thing on?" },
{ date: 20160102, value: "What should I write?" },
{ date: 20160202, value: "This is the new body" }
],
tags: [
{ date: 20160101, value: [ "test", "trivial" ] },
{ date: 20160102, value: [ "foo", "test" ] }
],
comments: [
{
author: "joe", // Unversioned field
body: [
{ date: 20160301, value: "Something cool" }
]
},
{
author: "xxx",
body: [
{ date: 20160101, value: "Spam" },
{ date: 20160102, deleted: true }
]
},
{
author: "jim",
body: [
{ date: 20160101, value: "Not bad" },
{ date: 20160102, value: "Not bad at all" }
]
}
]
}
答案 4 :(得分:0)
对于使用Python的用户(当然是python 3+以及更高版本),HistoricalCollection是pymongo的Collection对象的扩展。
文档示例:
from historical_collection.historical import HistoricalCollection
from pymongo import MongoClient
class Users(HistoricalCollection):
PK_FIELDS = ['username', ] # <<= This is the only requirement
# ...
users = Users(database=db)
users.patch_one({"username": "darth_later", "email": "darthlater@example.com"})
users.patch_one({"username": "darth_later", "email": "darthlater@example.com", "laser_sword_color": "red"})
list(users.revisions({"username": "darth_later"}))
# [{'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
# 'username': 'darth_later',
# 'email': 'darthlater@example.com',
# '_revision_metadata': None},
# {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
# 'username': 'darth_later',
# 'email': 'darthlater@example.com',
# '_revision_metadata': None,
# 'laser_sword_color': 'red'}]
完全公开,我是软件包的作者。 :)