Управление версиями содержимого базы данных

16

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

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

Я могу придумать несколько способов реализовать собственное отслеживание изменений, но все они кажутся довольно грубыми:

  • Сохраняйте всю строку при каждом изменении, связывайте строку с идентификатором источника с помощью первичного ключа (то, к чему я сейчас склоняюсь, это самое простое). Однако, множество небольших изменений может привести к большому раздутию таблицы.
  • сохранить до / после / пользователя / временную метку для каждого изменения с именем столбца, чтобы связать изменение с соответствующим столбцом.
  • сохранить до / после / пользователя / отметку времени с таблицей для каждого столбца (приведет к слишком большому количеству таблиц).
  • сохраняйте diffs / user / timestamp для каждого изменения со столбцом (это будет означать, что вам придется пройти всю промежуточную историю изменений, чтобы вернуться к определенной дате).

Каков наилучший подход здесь? Похоже, я переворачиваю свою собственную (лучшую) кодовую базу.


Бонусные баллы для PostgreSQL.

Поддельное имя
источник
Этот вопрос уже обсуждался на SO: stackoverflow.com/questions/3874199/… . Google для "истории записей базы данных", и вы найдете еще несколько статей.
Док Браун
1
Похоже, идеальный кандидат на Event Sourcing
Джеймс
Почему бы не использовать журнал транзакций SQL-сервера для достижения цели?
Томас Джанк

Ответы:

11

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

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

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

kiwiron
источник
Обратите внимание, что другие механизмы базы данных могут вести себя по-разному, например, MySQL допускает несколько значений NULL в столбце с уникальным индексом. Это делает это ограничение намного более трудным для выполнения.
2015 г.
Использовать фактическую временную метку небезопасно, но некоторые базы данных MVCC работают внутренне, сохраняя минимальные и максимальные серийные номера транзакций вместе с кортежами.
user2313838
«Это легко с Oracle, так как уникальный индекс может содержать один и только один ноль». Неправильно. Oracle вообще не включает нулевые значения в индексы. Количество столбцов с уникальным индексом не ограничено.
Джеррат
@Gerrat Прошло много лет с тех пор, как я разработал базу данных, в которой было это требование, и у меня больше нет доступа к этой базе данных. Вы правы, что стандартный уникальный индекс может поддерживать несколько нулей, но я думаю, что мы использовали либо уникальное ограничение, либо, возможно, функциональный индекс.
Кивирон
8

Обратите внимание, что если вы используете Microsoft SQL Server, для этого уже есть функция, называемая « Изменение сбора данных» . Вам все еще нужно будет написать код для доступа к предыдущим ревизиям позже (CDC создает для этого определенные представления), но, по крайней мере, вам не нужно ни изменять схему ваших таблиц, ни реализовывать само отслеживание изменений.

Под капотом происходит следующее:

  • CDC создает дополнительную таблицу, содержащую ревизии,

  • Ваша исходная таблица используется как и прежде, то есть любое обновление отражается в этой таблице напрямую,

  • В таблице CDC хранятся только измененные значения, что означает, что дублирование данных сведено к минимуму.

Тот факт, что изменения хранятся в другой таблице, имеет два основных последствия:

  • Выборы из исходной таблицы выполняются так же быстро, как и без CDC. Если я хорошо помню, CDC происходит после обновления, поэтому обновления происходят одинаково быстро (хотя я плохо помню, как CDC управляет согласованностью данных).

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

Арсений Мурзенко
источник
6

Решите проблему «философски» и сначала в коде. А затем «договориться» с кодом и базой данных, чтобы это произошло.

Например , если вы имеете дело с общими статьями, первоначальная концепция статьи может выглядеть следующим образом:

class Article {
  public Int32 Id;
  public String Body;
}

И на следующем базовом уровне я хочу сохранить список изменений:

class Article {
  public Int32 Id;
  public String Body;
  public List<String> Revisions;
}

И мне может показаться, что текущее тело - только последняя ревизия. И это означает две вещи: мне нужно, чтобы каждая Ревизия была датирована или пронумерована:

class Revision {
  public Int32 Id;
  public Article ParentArticle;
  public DateTime Created;
  public String Body;
}

И ... и текущее тело статьи не должно отличаться от последней редакции:

class Article {
  public Int32 Id;
  public String Body {
    get {
      return (Revisions.OrderByDesc(r => r.Created))[0];
    }
    set {
      Revisions.Add(new Revision(value));
    }
  }
  public List<Revision> Revisions;
}

Несколько деталей отсутствуют; но это показывает, что вы, вероятно, хотите две сущности . Один представляет статью (или другой тип заголовка), а другой представляет собой список ревизий (группировка любых полей имеет хороший «философский» смысл для группировки). Изначально вам не нужны специальные ограничения базы данных, потому что ваш код не заботится ни о каких ревизиях сам по себе - это свойства статьи, которая знает о ревизиях.

Таким образом, вам не нужно беспокоиться о том, чтобы помечать ревизии каким-либо особым образом или полагаться на ограничение базы данных, чтобы отметить «текущую» статью. Вы просто отметите их время (даже с идентификатором auto-inc'd), сделаете их связанными с их родительской статьей, и дайте статье знать, что самая последняя из них - самая последняя.

И вы позволяете ORM обрабатывать менее философские детали - или вы скрываете их в пользовательском служебном классе, если вы не используете готовый ORM.

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

svidgen
источник
2

Существует вики-страница PostgreSQL для триггера отслеживания аудита, в котором рассказывается, как настроить журнал аудита, который будет делать то, что вам нужно.

Он отслеживает полные исходные данные изменения, а также список новых значений для обновлений (для вставок и удалений есть только одно значение). Если вы хотите восстановить старую версию, вы можете получить копию исходных данных из записи аудита. Обратите внимание, что если ваши данные содержат внешние ключи, эти записи также необходимо откатить, чтобы обеспечить согласованность.

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

Если строки, которые вы отслеживаете, очень большие, и пространство представляет серьезную проблему, вы можете попытаться разбить изменения и сохранить минимальные различия / патчи, но это определенно больше работы, чтобы охватить все ваши типы данных. Я делал это раньше, и было сложно восстанавливать старые версии данных, просматривая все изменения в обратном порядке, по одному за раз.

Бен Тернер
источник
1

Ну, я просто выбрал простейший вариант - триггер, который копирует старую версию строки в журнал истории для каждой таблицы.

Если у меня будет слишком много раздувания базы данных, я могу посмотреть, возможно ли сворачивать некоторые незначительные изменения в истории, если это необходимо.

Решение оказалось довольно грязным, так как я хотел автоматически генерировать триггерные функции. Я являюсь SQLAlchemy, поэтому я смог создать таблицу истории, выполнив некоторые переходы по наследованию, что было неплохо, но фактические функции триггера завершились, потребовав некоторого объединения строк для правильной генерации функций PostgreSQL и отображения столбцов из одной таблицы в другой правильно.

В любом случае, все это на github здесь .

Поддельное имя
источник