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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.