Подтвердить что ты не робот

MongoDB/NoSQL: ведение истории изменений документов

Довольно распространенное требование в приложениях базы данных - отслеживать изменения одного или нескольких конкретных объектов в базе данных. Я слышал, что это называется версией строк, таблицей журналов или таблицей истории (я уверен, что для этого есть другие имена). Есть несколько способов приблизиться к нему в СУБД - вы можете записывать все изменения из всех исходных таблиц в одну таблицу (больше из журнала) или иметь отдельную таблицу истории для каждой исходной таблицы. У вас также есть возможность либо управлять протоколированием кода приложения, либо с помощью триггеров базы данных.

Я пытаюсь понять, как будет выглядеть решение той же проблемы в базе данных NoSQL/document (в частности, MongoDB) и как она будет решена единообразно. Было бы так же просто, как создавать номера версий для документов и никогда не перезаписывать их? Создание отдельных коллекций для "реальных" и "зарегистрированных" документов? Как это повлияет на запросы и производительность?

В любом случае, является ли это распространенным сценарием с базами данных NoSQL, и если да, существует ли общее решение?

4b9b3361

Ответ 1

Хороший вопрос, я тоже изучал это сам.

Создайте новую версию при каждом изменении

Я столкнулся с модулем версии драйвера Mongoid для 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 }
  ]
}

С каждым из этих подходов вы можете сохранить обновленную и сглаженную версию в одной коллекции и данные истории в отдельной коллекции. Это должно улучшить время запроса, если вас интересует только последняя версия документа. Но когда вам нужны как последняя версия, так и исторические данные, вам нужно выполнить два запроса, а не один. Поэтому выбор использования отдельной коллекции по сравнению с двумя отдельными коллекциями должен зависеть от того, как часто вашему приложению нужны исторические версии.

Большая часть этого ответа - это просто свалка моих мыслей, я на самом деле этого не пытался. Оглядываясь назад, первый вариант, вероятно, является самым простым и лучшим решением, если только накладные расходы на дубликаты данных не очень важны для вашего приложения. Второй вариант довольно сложный и, вероятно, не стоит усилий. Третий вариант - это, в основном, оптимизация варианта 2, и его следует упростить, но, вероятно, это не стоит усилий по внедрению, если вы действительно не можете пойти с вариантом.

С нетерпением ждем отзывов об этом и других людей, которые могут решить эту проблему:)

Ответ 2

Мы частично внедрили это на нашем сайте, и мы используем "Редакции магазина в отдельном документе" (и отдельную базу данных). Мы написали пользовательскую функцию для возврата diff и сохранили ее. Не так сложно и можно разрешить автоматическое восстановление.

Ответ 3

Почему не вариант Сохранять изменения в документе?

Вместо хранения версий по каждой паре ключей текущие пары ключей в документе всегда представляют собой самое последнее состояние, а "журнал" изменений хранится в массиве истории. Только те ключи, которые изменились с момента создания, будут иметь запись в журнале.

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

Ответ 4

У вас может быть текущая база данных NoSQL и историческая база данных NoSQL. Ежедневно будет проходить ежедневный ETL. Этот ETL будет записывать каждое значение с отметкой времени, поэтому вместо значений он всегда будет кортежей (поля версии). Он будет записывать только новое значение, если произошли изменения в текущем значении, экономя место в этом процессе. Например, этот исторический файл json базы данных NoSQL может выглядеть так:

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

Ответ 5

Для пользователей Python (python 3+ и выше) существует HistoricalCollection, которая является расширением объекта Collection pymongo.

Пример из документов:

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": "[email protected]"})
users.patch_one({"username": "darth_later", "email": "[email protected]", "laser_sword_color": "red"})

list(users.revisions({"username": "darth_later"}))

# [{'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': '[email protected]',
#   '_revision_metadata': None},
#  {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': '[email protected]',
#   '_revision_metadata': None,
#   'laser_sword_color': 'red'}]

Полное раскрытие, я автор пакета. :)