Как контролировать версию записи в базе данных

177

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

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

Нильс Босма
источник

Ответы:

164

Допустим, у вас есть FOOтаблица, которую администраторы и пользователи могут обновить. Большую часть времени вы можете писать запросы к таблице FOO. Счастливые дни.

Затем я бы создал FOO_HISTORYтаблицу. Здесь есть все столбцы FOOтаблицы. Первичный ключ такой же, как FOO плюс столбец RevisionNumber. Существует внешний ключ от FOO_HISTORYдо FOO. Вы также можете добавить столбцы, связанные с ревизией, такие как UserId и RevisionDate. Заполните номера RevisionNumbers постоянно увеличивающимися во всех *_HISTORYтаблицах (т.е. из последовательности Oracle или эквивалентной). Не полагайтесь только на одно изменение в секунду (т.е. не вводите RevisionDateв первичный ключ).

Теперь, каждый раз , когда вы обновляете FOO, просто , прежде чем делать обновление вставить старые значения в FOO_HISTORY. Вы делаете это на каком-то фундаментальном уровне в вашем дизайне, чтобы программисты не могли случайно пропустить этот шаг.

Если вы хотите удалить строку из FOOвас есть несколько вариантов. Либо каскадно удалите всю историю, либо выполните логическое удаление, пометив его FOOкак удаленное.

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

WW.
источник
1
Вы можете выполнить обновление таблицы аудита с помощью триггеров базы данных, если ваш уровень доступа к данным не поддерживает это напрямую. Кроме того, несложно построить генератор кода для создания триггеров, использующих самоанализ из словаря системных данных.
ConcernedOfTunbridgeWells
44
Я рекомендую вам на самом деле вставить новые данные, а не предыдущие, поэтому в таблице истории есть все данные. Хотя он хранит избыточные данные, он исключает особые случаи, необходимые для поиска в обеих таблицах, когда требуются исторические данные.
Nerdfest
6
Лично я бы порекомендовал не удалять ничего (перенести это на конкретную домашнюю деятельность) и иметь столбец «тип действия», чтобы указать, является ли он вставкой / обновлением / удалением. Для удаления вы копируете строку как обычно, но ставите «удалить» в столбце типа действия.
Нил Барнвелл
3
@Hydrargyrum Таблица, содержащая текущие значения, будет работать лучше, чем представление исторической таблицы. Вы также можете определить внешние ключи, ссылающиеся на текущие значения.
WW.
2
There is a foreign key from FOO_HISTORY to FOO': плохая идея, я хотел бы удалить записи из foo без изменения истории. таблица истории должна быть только для вставки при обычном использовании.
Jasen
46

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

Лучший пример, который приходит на ум, это MediaWiki, движок Wikipedia. Сравните схему базы данных здесь , в частности таблицу ревизий .

В зависимости от того, какие технологии вы используете, вам придется найти несколько хороших алгоритмов сравнения / слияния.

Проверьте этот вопрос, если это для .NET.

CMS
источник
30

В мире BI это можно сделать, добавив startDate и endDate в таблицу, для которой вы хотите создать версию. Когда вы вставляете первую запись в таблицу, startDate заполняется, но endDate является нулевым. Когда вы вставляете вторую запись, вы также обновляете endDate первой записи на startDate второй записи.

Когда вы хотите просмотреть текущую запись, вы выбираете ту, в которой endDate равен нулю.

Это иногда называют медленно изменяющимся размером 2 типа . Смотрите также TupleVersioning

Дэйв Нили
источник
Разве мой стол не станет достаточно большим при таком подходе?
Нильс Босма
1
Да, но вы можете справиться с этим, проиндексировав и / или разделив таблицу. Кроме того, будет только небольшая горстка больших столов. Большинство будет намного меньше.
ConcernedOfTunbridgeWells
Если я не ошибаюсь, единственным недостатком здесь является то, что он ограничивает изменения раз в секунду правильно?
pimbrouwers
@pimbrouwers да, в конечном итоге это зависит от точности полей и функции, которая их заполняет.
Дейв Нили
9

Обновление до SQL 2008.

Попробуйте использовать отслеживание изменений SQL в SQL 2008. Вместо взлома временных меток и надгробных столбцов вы можете использовать эту новую функцию для отслеживания изменений данных в вашей базе данных.

Отслеживание изменений MSDN SQL 2008

D3vtr0n
источник
7

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

wuher
источник
6

Два варианта:

  1. Иметь таблицу истории - вставляйте старые данные в эту таблицу истории при каждом обновлении оригинала.
  2. Таблица аудита - сохраните значения до и после - только для измененных столбцов в таблице аудита вместе с другой информацией, например, кто обновил и когда.
Alok
источник
5

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

Дополнительным преимуществом этого подхода является то, что аудит будет выполняться независимо от того, была ли операция sql выполнена через ваши DLL-библиотеки доступа к данным или через SQL-запрос вручную; (так как аудит выполняется на самом сервере).

Доктор джонс
источник
3

Вы не говорите, какая база данных, а я не вижу ее в тегах записей. Если это для Oracle, я могу рекомендовать подход, встроенный в Designer: использовать журнальные таблицы . Если это для какой-либо другой базы данных, ну, в принципе, я тоже рекомендую то же самое ...

То, как это работает, в случае, если вы хотите скопировать его в другую БД, или, может быть, если вы просто хотите это понять, заключается в том, что для таблицы также создается теневая таблица, просто обычная таблица базы данных с теми же характеристиками полей плюс некоторые дополнительные поля: например, какое действие было выполнено в последний раз (строка, типичные значения «INS» для вставки, «UPD» для обновления и «DEL» для удаления), дата и время, когда было выполнено действие, и идентификатор пользователя, который сделал Это.

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

В Oracle все, что вам нужно, генерируется автоматически как код SQL, все, что вам нужно сделать, это скомпилировать / запустить его; и это идет с основным приложением CRUD (фактически только "R"), чтобы осмотреть это.

Барт
источник
2

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

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

Таким образом, если критерии урока удалены или перемещены, их результаты не изменятся.

В настоящее время я делаю это, обрабатывая все данные в одной таблице. Обычно у меня было бы только одно поле id, но в этой системе я использую id и sub_id. Sub_id всегда остается со строкой, через обновления и удаления. Идентификатор автоматически увеличивается. Программное обеспечение плана урока свяжется с самым новым sub_id. Результаты студента будут ссылаться на идентификатор. Я также включил отметку времени для отслеживания, когда произошли изменения, но нет необходимости обрабатывать управление версиями.

Одна вещь, которую я мог бы изменить, после того как я проверил это, я мог бы использовать ранее упомянутую идею нулевого конца endDate. В моей системе, чтобы найти самую новую версию, мне нужно найти max (id). Другая система просто ищет endDate = null. Не уверен, что преимущества получаются вне зависимости от наличия другого поля даты.

Мои два цента.

Иордания
источник
2

Пока @WW. Ответ - хороший ответ. Другой способ - создать столбец версий и сохранить все версии в одной таблице.

Для одного стола подходите либо:

  • Используйте флаг, чтобы указать последние слова Word Press
  • ИЛИ сделать противную больше версии outer join.

Пример SQL outer joinметода, использующего номера ревизий:

SELECT tc.*
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL 
AND tc.path = '/stuff' -- path in this case is our natural id.

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

Пример создания новой ревизии для '/stuff'может быть:

INSERT INTO text_content (id, path, data, revision, revision_comment, enabled, create_time, update_time)
(
SELECT
(md5(random()::text)) -- {id}
, tc.path
, 'NEW' -- {data}
, (tc.revision + 1)
, 'UPDATE' -- {comment}
, 't' -- {enabled}
, tc.create_time
, now() 
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL 
AND tc.path = '/stuff' -- {path}
)

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

Подход с использованием флагов и таблицы истории требует вставки / обновления двух строк.

Другое преимущество outer joinподхода с использованием номера ревизии состоит в том, что вы всегда можете позже выполнить рефакторинг для подхода с несколькими таблицами с помощью триггеров, потому что ваш триггер должен, по существу, делать что-то подобное вышеописанному.

Адам Гент
источник
2

Алок предложил Audit tableвыше, я хотел бы объяснить это в своем посте.

Я принял этот проект без схемы, единый стол в моем проекте.

Схема:

  • id - INTEGER AUTO INCREMENT
  • имя пользователя - STRING
  • имя таблицы - STRING
  • oldvalue - TEXT / JSON
  • новое значение - TEXT / JSON
  • созданные на - DATETIME

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

Плюсы с этим дизайном:

  • Меньшее количество таблиц для управления историей.
  • Хранит полный снимок каждой строки старого и нового состояния.
  • Легко искать на каждом столе.
  • Можно создать раздел по таблице.
  • Можно определить политику хранения данных для каждой таблицы.

Минусы с этим дизайном:

  • Размер данных может быть большим, если в системе происходят частые изменения.
Хасан Фарид
источник