MongoDB / NoSQL:ドキュメントの変更履歴の保持


134

データベースアプリケーションでかなり一般的な要件は、データベース内の1つ以上の特定のエンティティへの変更を追跡することです。これは行のバージョン管理、ログテーブル、または履歴テーブルと呼ばれます(他にも名前があるはずです)。RDBMSでこれにアプローチする方法はいくつかあります。すべてのソーステーブルからのすべての変更を単一のテーブル(ログの詳細)に書き込むか、ソーステーブルごとに個別の履歴テーブルを作成できます。また、ロギングをアプリケーションコードで管理するか、データベーストリガーを介して管理するかを選択できます。

同じ問題の解決策がNoSQL /ドキュメントデータベース(具体的にはMongoDB)でどのように見えるか、そしてそれがどのように統一された方法で解決されるかを考えています。ドキュメントのバージョン番号を作成するのと同じくらい簡単で、それらを上書きすることはありませんか?「実際の」ドキュメントと「ログに記録された」ドキュメントのコレクションを個別に作成しますか?これはクエリとパフォーマンスにどのように影響しますか?

とにかく、これはNoSQLデータベースの一般的なシナリオですか?その場合、一般的な解決策はありますか?


どの言語ドライバーを使用していますか?
ジョシュアパルトギ2010

まだ決定されていません-いまだにいじくり回されていて、バックエンドの選択をまだ確定していません(MongoDBは非常に可能性が高いように見えますが)。私はNoRM(C#)をいじくり回しており、そのプロジェクトに関連付けられている名前のいくつかが好きなので、それが選択される可能性が非常に高いようです。
Phil Sandler

2
私はこれが古い質問であることを知っていますが、MongoDBでバージョニングを探しているすべての人にとって、このSOの質問は関連があり、私の意見ではより良い答えがあります。
AWolf 2016年

回答:


107

良い質問です。私もこれを調べていました。

変更ごとに新しいバージョンを作成する

RubyのMongoidドライバーのバージョン管理モジュールに出くわしました。私自身は使用していませんが、見つけたものから、各ドキュメントにバージョン番号を追加しています。古いバージョンはドキュメント自体に埋め込まれています。主な欠点は、変更のたびにドキュメント全体が複製されるため、大きなドキュメントを処理しているときに、大量の重複コンテンツが保存されることになります。このアプローチは、小さいサイズのドキュメントを処理している場合や、ドキュメントを頻繁に更新しない場合に適しています。

変更を新しいバージョンにのみ保存する

別のアプローチは、変更されたフィールドのみを新しいバージョン格納することです。次に、履歴を「フラット化」して、ドキュメントの任意のバージョンを再構築できます。ただし、アプリケーションが最新のドキュメントを再構築できるようにモデルの変更を追跡し、更新と削除を保存する必要があるため、これはかなり複雑です。フラットな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つのコレクションに保存し、履歴データを別のコレクションに保存できます。ドキュメントの最新バージョンのみに関心がある場合、これによりクエリ時間が改善されます。ただし、最新バージョンと履歴データの両方が必要な場合は、1つではなく2つのクエリを実行する必要があります。したがって、単一のコレクションを使用するか、2つの別個のコレクションを使用するかの選択は、アプリケーションが履歴バージョンを必要とする頻度に依存するはずです

この答えの大部分は私の考えの頭脳のダンプです。私は実際にこれをまだ試していません。振り返ってみると、重複データのオーバーヘッドがアプリケーションにとって非常に重要でない限り、最初のオプションがおそらく最も簡単で最良のソリューションです。2番目のオプションは非常に複雑で、おそらく努力する価値はありません。3番目のオプションは基本的にオプション2の最適化であり、実装が容易ですが、実際にオプション1で実行できない場合を除いて、実装する価値はありません。

これに関するフィードバック、および問題に対する他の人々の解決策を楽しみにしています:)


デルタをどこかに保存して、履歴ドキュメントを取得するためにフラット化し、常に最新の情報を入手できるようにするにはどうすればよいでしょうか。
jpmc26 2013

@ jpmc26これは2番目のアプローチに似ていますが、デルタを保存して最新バージョンを取得する代わりに、デルタを保存して履歴バージョンを取得します。どちらの方法を使用するかは、履歴バージョンが必要になる頻度によって異なります。
Niels van der Rest

ドキュメントを現在の状態のビューとして使用し、タイムスタンプを含む各変更を追跡する変更ログとして2番目のドキュメントを作成することに関する段落を追加できます(初期値はこのログに表示される必要があります)。 '任意の時点に関連付け、たとえば、アルゴリズムがそれに触れたときに何が起こっていたかを関連付けたり、ユーザーがクリックしたときにアイテムがどのように表示されたかを確認したりします。
Manuel Arwed Schmidt 2015

インデックス付きフィールドが配列として表されている場合、これはパフォーマンスに影響しますか?
DmitriD 2017年

@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+以降)のユーザー向けに、pymongoのCollectionオブジェクトの拡張であるHistoricalCollectionがあります。

ドキュメントの例:

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

完全な開示、私はパッケージの作者です。:)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.