ChatGPT解决这个技术问题 Extra ChatGPT

MongoDB/NoSQL:保存文档更改历史

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

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

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

您使用的是什么语言驱动程序?
尚未决定——仍在修补,甚至还没有最终确定后端的选择(尽管 MongoDB 看起来极有可能)。我一直在修改 NoRM (C#),并且我喜欢与该项目相关的一些名称,因此它似乎很可能是选择。
我知道这是一个老问题,但对于任何正在寻找使用 MongoDB 进行版本控制的人来说,这个 SO question 是相关的,并且在我看来有更好的答案。

N
Niels van der Rest

好问题,我自己也在研究这个问题。

在每次更改时创建一个新版本

我遇到了 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 }
  ]
}

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

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

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


将增量存储在某个地方怎么样,这样您就必须展平以获取历史文档并始终保持当前可用?
@jpmc26 这类似于第二种方法,但不是保存增量以获取最新版本,而是保存增量以获取历史版本。使用哪种方法取决于您需要历史版本的频率。
您可以添加一段关于使用文档作为当前事物状态的视图并将第二个文档作为更改日志来跟踪每个更改,包括时间戳(初始值需要出现在此日志中) - 然后您可以“重播” ' 到任何给定的时间点,例如关联您的算法触摸它时发生的事情,或者查看用户单击项目时的显示方式。
如果索引字段表示为数组,这会影响性能吗?
@All-您能否分享一些代码来实现这一点?
P
Paul Taylor

为什么不在文档中对 Store 进行更改?

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

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

将文档存储在新集合中而不是存储在其中不是一个好主意吗?每次我们查询文档时,它都会随之而来,并且大量的更改会增加数据量。
@kirtan403 Mongo 查询有预测,只会返回您投影的内容,因此您自己决定文档的哪些部分出现。
BSON 文档的最大大小为 16 兆字节。
A
Amala

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


你能分享一些相同的代码吗?这种方法看起来很有希望
@smilyface - Spring Boot Javers 集成是实现这一目标的最佳选择
@PAA - 我问了一个问题(几乎相同的概念)。 stackoverflow.com/questions/56683389/…您对此有什么意见吗?
P
Paul Kar.

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

D
Dash2TheDot

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

完全披露,我是包作者。 :)