MongoDB/NoSQL: 保持文档更改历史记录

数据库应用程序中一个相当常见的需求是跟踪对数据库中一个或多个特定实体的更改。我听说过这种方法叫做行版本控制、日志表或者历史表(我肯定还有其他的名字)。在 RDBMS 中有许多方法可以处理它——您可以将所有源表的所有更改写入单个表(更多的是日志) ,或者为每个源表创建一个单独的历史表。您还可以选择在应用程序代码中管理日志记录,或者通过数据库触发器管理日志记录。

我正在尝试思考同一个问题的解决方案在 NoSQL/document 数据库(特别是 MongoDB)中会是什么样子,以及如何以一种统一的方式来解决它。它是否会像为文档创建版本号那样简单,而且永远不会覆盖它们?为“真实”和“已记录”文档创建单独的集合?这将如何影响查询和性能?

无论如何,这是 NoSQL 数据库的常见场景吗? 如果是的话,有没有通用的解决方案?

73855 次浏览

问得好,我自己也在调查这件事。

在每个更改上创建一个新版本

我偶然发现了 Ruby 的蒙古语驱动程序的 版本控制模块。我自己还没有使用过它,但是从 我能找到什么开始,它为每个文档添加了一个版本号。旧版本嵌入在文档本身中。主要的缺点是 每次更改都会复制整个文档,这将导致在处理大型文档时存储大量重复的内容。当处理小型文档和/或不经常更新文档时,这种方法很好。

只在新版本中存储更改

另一种方法是 只在新版本中存储更改后的字段。然后,您可以“压平”您的历史记录来重建文档的任何版本。但是这相当复杂,因为您需要跟踪模型中的更改,并以应用程序可以重新构建最新文档的方式存储更新和删除。这可能有些棘手,因为您要处理的是结构化文档,而不是平面 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 }
]
}

使用这些方法中的每一种,您都可以在一个集合中存储最新的扁平化版本,并在单独的集合中存储历史数据。如果您只对文档的最新版本感兴趣,这将提高查询时间。但是,当需要最新版本和历史数据时,需要执行两个查询,而不是一个查询。因此,使用单个集合还是使用两个独立集合的选择应该取决于 您的应用程序需要历史版本的频率是多少

这个答案大部分只是我的一些想法,我还没有真正尝试过这些。回过头来看,第一个选项可能是最简单和最好的解决方案,除非重复数据的开销对于您的应用程序非常重要。第二种选择相当复杂,可能不值得花费精力。第三个选项基本上是对第二个选项的优化,应该更容易实现,但可能不值得付出实现努力,除非你真的不能选择第一个选项。

期待这方面的反馈,以及其他人对这个问题的解决方案:)

我们已经在我们的网站上部分实现了这一点,并且我们使用了“在单独的文档中存储修订版”(和单独的数据库)。我们写了一个自定义函数来返回差值,然后存储它。不太难,可以自动恢复。

可以有一个当前的 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" }
]
}
]
}

为什么不是 在文档中存储更改的变体呢?

文档中的当前密钥对总是代表最近的状态,并且更改的“日志”存储在历史数组中,而不是针对每个密钥对存储版本。只有那些自创建以来已经更改的键将在日志中有一个条目。

{
_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" }
}
]
}

对于 Python 的用户(python3 + ,当然还有更高版本) ,有一个 历史收藏,它是 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'}]

完全公开,我是软件包的作者。 :)