【问题标题】:MongoDB/NoSQL: Keeping Document Change HistoryMongoDB/NoSQL:保存文档更改历史
【发布时间】:2011-03-31 06:34:50
【问题描述】:

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

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

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

【问题讨论】:

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

标签: mongodb nosql


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

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

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

    {
      _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 查询有投影,只会返回您投影的内容,因此您自己决定文档的哪些部分。
    【解决方案3】:

    一个人可以拥有一个当前的 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】:

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

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

      我遇到了 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 - 你能分享一些代码来实现这个吗?
      【解决方案5】:

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

      【讨论】:

      • 你能分享一些相同的代码吗?这种方法看起来很有前景
      • @smilyface - Spring Boot Javers 集成是实现这一目标的最佳选择
      • @PAA - 我问了一个问题(几乎相同的概念)。 stackoverflow.com/questions/56683389/…你有什么意见吗?
      猜你喜欢
      • 1970-01-01
      • 2019-01-05
      • 2014-07-08
      • 2012-12-31
      • 1970-01-01
      • 1970-01-01
      • 2012-11-25
      • 1970-01-01
      • 2020-01-28
      相关资源
      最近更新 更多