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

134

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

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

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

Фил Сэндлер
источник
Какой языковой драйвер вы используете?
Джошуа Партоги
Еще не решил - все еще ковыряюсь и еще даже не доработал выбор серверных частей (хотя MongoDB выглядит крайне вероятным). Я возился с NoRM (C #), и мне нравятся некоторые имена, связанные с этим проектом, так что это, скорее всего, будет правильным выбором.
Фил Сэндлер
2
Я знаю, что это старый вопрос, но для тех, кто ищет версионирование с помощью MongoDB, этот вопрос SO связан и, по моему мнению, с лучшими ответами.
AWolf 01

Ответы:

107

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

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

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

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

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

Жду отзывов по этому поводу и других решений проблемы :)

Нильс ван дер Рест
источник
А как насчет того, чтобы где-то хранить дельты, чтобы вам пришлось сгладить, чтобы получить исторический документ и всегда иметь текущий доступный?
jpmc26
@ jpmc26 Это похоже на второй подход, но вместо сохранения дельт для перехода к последним версиям вы сохраняете дельты, чтобы перейти к историческим версиям. Какой подход использовать, зависит от того, как часто вам понадобятся исторические версии.
Niels van der Rest
Вы можете добавить абзац об использовании документа в качестве представления о текущем состоянии вещей и наличии второго документа в качестве журнала изменений, который будет отслеживать каждое изменение, включая временную метку (в этом журнале должны отображаться начальные значения) - затем вы можете «воспроизвести» 'с любым заданным моментом времени и, например, сопоставить то, что происходило, когда ваш алгоритм коснулся его, или посмотреть, как элемент отображался, когда пользователь щелкнул по нему.
Мануэль Арвед Шмидт
Повлияет ли это на производительность, если индексированные поля представлены в виде массивов?
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 будет записывать каждое значение с меткой времени, поэтому вместо значений всегда будут кортежи (версионные поля). Он будет записывать новое значение только в том случае, если в текущее значение было внесено изменение, что позволяет сэкономить место в процессе. Например, этот исторический 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" }
      ]
    }
  ]
}
Пол Кар.
источник
0

Для пользователей 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": "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'}]

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

Dash2TheDot
источник