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


134

数据库应用程序中一个相当普遍的要求是跟踪对数据库中一个或多个特定实体的更改。我听说过这称为行版本控制,日志表或历史记录表(我敢肯定还有其他名称)。在RDBMS中有多种方法可以实现它-您可以将所有源表中的所有更改都写入单个表(更多日志),或者为每个源表都有单独的历史表。您还可以选择管理应用程序代码的登录或通过数据库触发器进行管理。

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

无论如何,这是NoSQL数据库的常见情况,如果是,是否有常见的解决方案?


您正在使用哪种语言驱动程序?
约书亚·帕托吉

尚未决定-仍在修改,甚至尚未最终确定后端的选择(尽管MongoDB 有可能出现)。我一直在修改NoRM(C#),而且我喜欢与该项目相关的一些名称,因此似乎很可能是您的选择。
菲尔·桑德勒

2
我知道这是一个古老的问题,但是对于任何希望使用MongoDB进行版本控制的人来说,这个SO问题都是相关的,而且我认为它有更好的答案。
AWolf '16

Answers:


107

好问题,我自己也正在调查。

每次更改都创建一个新版本

我遇到了Ruby的Mongoid驱动程序的Versioning模块。我还没有亲自使用过它,但是从我发现的情况来看,它会为每个文档添加一个版本号。旧版本嵌入在文档本身中。主要缺点是,每次更改都会复制整个文档,这将导致在处理大型文档时存储大量重复内容。这种方法很好,但是当您处理小型文档和/或不经常更新文档时。

仅将更改存储在新版本中

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

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

这个答案大部分只是我思想的转储,我实际上还没有尝试过。回顾一下,第一个选择可能是最简单,最好的解决方案,除非重复数据的开销对您的应用程序而言非常重要。第二种选择非常复杂,可能不值得付出努力。第三种选择基本上是对第二种选择的优化,应该更容易实现,但是除非您真的不能采用第一种选择,否则可能不值得花大力气进行实施。

期待对此反馈以及其他人对问题的解决方案:)


如何将增量存储在某个地方,以便您必须拼凑以获取历史文档并始终获取最新文档?
jpmc26

@ jpmc26这类似于第二种方法,但是您没有保存增量以获取最新版本,而是保存了增量以获取历史版本。使用哪种方法取决于您需要多久使用一次历史版本。
Niels van der Rest

您可以添加一段关于使用文档作为当前事物状态的视图的段落,并添加第二个文档作为变更日志,以跟踪包括时间戳在内的每个变更(初始值需要出现在此日志中)-然后,您可以“重放到任何给定的时间点,例如关联算法触摸时发生的情况或查看用户单击时项目的显示方式。
Manuel Arwed Schmidt

如果将索引字段表示为数组,这会影响性能吗?
DmitriD

@All-您能否分享一些代码来实现这一目标?
Pra_A

8

我们已经在网站上部分实现了此功能,并使用了“在单独的文档中存储修订”(和单独的数据库)。我们编写了一个自定义函数来返回差异并进行存储。不那么困难,并且可以进行自动恢复。


2
您能否在相同的地方共享一些代码?这种方法看起来很有希望
Pra_A

1
@smilyface-Spring Boot Javers集成最能实现这一目标
Pra_A

@PAA-我问了一个问题(几乎相同的概念)。stackoverflow.com/questions/56683389 / ...您对此有任何输入吗?
smilyface,

6

为什么不对文档中的存储更改进行更改

文档中的当前密钥对始终代表最新状态,而不是针对每个密钥对存储版本,而是将更改的“日志”存储在历史记录数组中。仅那些自创建以来已更改的键在日志中才会有一个条目。

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

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

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'}]

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

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.