Дизайн базы данных для ревизий? - PullRequest
117 голосов
/ 02 сентября 2008

В проекте есть требование хранить все ревизии (История изменений) для сущностей в базе данных. В настоящее время у нас есть 2 разработанных предложения для этого:

например. для субъекта "Сотрудник"

Дизайн 1:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"

Дизайн 2:

-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

-- In this approach we have basically duplicated all the fields on Employees 
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName, 
      LastName, DepartmentId, .., ..)"

Есть ли другой способ сделать это?

Проблема с «Design 1» заключается в том, что нам приходится анализировать XML каждый раз, когда вам нужен доступ к данным. Это замедлит процесс и также добавит некоторые ограничения, например, мы не можем добавлять объединения в поля данных ревизий.

И проблема с «Проектом 2» заключается в том, что мы должны дублировать каждое поле во всех сущностях (у нас есть около 70-80 сущностей, для которых мы хотим сохранить исправления).

Ответы [ 16 ]

50 голосов
/ 02 сентября 2008

Я думаю, что ключевой вопрос, который нужно задать здесь: «Кто / что будет использовать историю»?

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

Создайте таблицу с именем 'AuditTrail' или что-то, имеющее следующие поля ...

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL

Затем вы можете добавить столбец «LastUpdatedByUserID» во все ваши таблицы, который следует устанавливать каждый раз, когда вы выполняете обновление / вставку в таблицу.

Затем вы можете добавить триггер к каждой таблице, чтобы перехватывать любую вставку / обновление, которое происходит, и создает запись в этой таблице для каждого измененного поля. Поскольку для таблицы также предоставляется 'LastUpdateByUserID' для каждого обновления / вставки, вы можете получить доступ к этому значению в триггере и использовать его при добавлении в таблицу аудита.

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

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

Просто мысль!

36 голосов
/ 02 сентября 2008
  1. Делайте , а не , помещайте все это в одну таблицу с атрибутом дискриминатора IsCurrent. Это просто вызывает проблемы в будущем, требует суррогатных ключей и всевозможных других проблем.
  2. В Design 2 действительно есть проблемы с изменениями схемы. Если вы изменяете таблицу Employees, вы должны изменить таблицу EmployeeHistories и все связанные с ней sprocs. Потенциально удваивает ваши усилия по изменению схемы.
  3. Дизайн 1 работает хорошо, и, если все сделано правильно, не стоит больших затрат с точки зрения производительности. Вы можете использовать XML-схему и даже индексы, чтобы преодолеть возможные проблемы с производительностью. Ваш комментарий о разборе xml действителен, но вы можете легко создать представление, используя xquery, которое вы можете включить в запросы и присоединиться к ним. Как то так ...
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId

SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,

  RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,

  RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,

FROM EmployeeHistories 
19 голосов
/ 24 сентября 2008

Статья History Tables в блоге Database Programmer может быть полезна - в ней рассматриваются некоторые вопросы, затронутые здесь, и обсуждается хранение дельт.

Редактировать

В эссе History Table автор ( Kenneth Downs ) рекомендует поддерживать таблицу истории, по крайней мере, из семи столбцов:

  1. метка времени изменения,
  2. Пользователь, который внес изменение,
  3. токен для идентификации записи, которая была изменена (где история ведется отдельно от текущего состояния),
  4. Независимо от того, было ли изменение вставкой, обновлением или удалением,
  5. старое значение,
  6. Новое значение,
  7. Дельта (для изменения числовых значений).

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

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

13 голосов
/ 08 июня 2013

Избегайте дизайна 1; это не очень удобно, если вам потребуется, например, выполнить откат к старым версиям записей - автоматически или «вручную» с помощью консоли администратора.

На самом деле я не вижу недостатков в Design 2. Я думаю, что вторая таблица History должна содержать все столбцы, присутствующие в первой таблице Records. Например. В MySQL вы можете легко создать таблицу с такой же структурой, что и другая таблица (create table X like Y). И когда вы собираетесь изменить структуру таблицы записей в вашей действующей базе данных, вам все равно придется использовать команды alter table - и эти команды не потребуют больших усилий и для вашей таблицы истории.

Примечания

  • Таблица записей содержит только последнюю редакцию;
  • Таблица истории содержит все предыдущие версии записей в таблице записей;
  • Первичный ключ таблицы истории - это первичный ключ таблицы записей с добавленным столбцом RevisionId;
  • Подумайте о дополнительных вспомогательных полях, таких как ModifiedBy - пользователь, создавший конкретную ревизию. Вы также можете иметь поле DeletedBy для отслеживания того, кто удалил конкретную ревизию.
  • Подумайте о том, что DateModified должно означать - либо это означает, где именно эта ревизия была создана, либо это будет означать, когда эта конкретная ревизия была заменена другой. Первый требует, чтобы поле было в таблице «Записи», и на первый взгляд кажется более интуитивным; однако второе решение представляется более практичным для удаленных записей (дата, когда эта конкретная редакция была удалена). Если вы выберете первое решение, вам, вероятно, понадобится второе поле DateDeleted (конечно, только если оно вам нужно). Зависит от вас и того, что вы на самом деле хотите записать.

Операции в Design 2 очень тривиальны:

Изменение
  • скопировать запись из таблицы записей в таблицу истории, присвоить ей новый RevisionId (если он еще не представлен в таблице записей), обработать DateModified (зависит от того, как вы ее интерпретируете, см. Примечания выше)
  • продолжить обычное обновление записи в таблице записей.
Удалить
  • сделать точно так же, как в первом шаге операции Modify. Обрабатывайте DateModified / DateDeleted соответственно, в зависимости от выбранной вами интерпретации.
Восстановление (или откат)
  • взять наивысшую (или некоторую конкретную?) Ревизию из таблицы истории и скопировать ее в таблицу записей
История изменений списка для конкретной записи
  • выберите из таблицы истории и таблицы записей
  • подумайте, что именно вы ожидаете от этой операции; он, вероятно, определит, какую информацию вам требуется из полей DateModified / DateDeleted (см. примечания выше)

Если вы зайдете в Design 2, все команды SQL, необходимые для этого, будут очень простыми, а также обслуживать! Возможно, будет намного проще, если вы будете использовать вспомогательные столбцы (RevisionId, DateModified) также в таблице записей - чтобы обе таблицы имели одинаковую структуру (за исключением уникальных ключей) ! Это позволит использовать простые команды SQL, которые будут терпимы к любым изменениям структуры данных:

insert into EmployeeHistory select * from Employe where ID = XX

Не забудьте использовать транзакции!

Что касается масштабирования , это решение очень эффективно, поскольку вы не преобразуете какие-либо данные из XML назад и вперед, просто копируете строки целой таблицы - очень простые запросы, используя индексы - очень эффективно!

13 голосов
/ 02 сентября 2008

Мы внедрили решение, очень похожее на решение, предложенное Крисом Робертсом, и оно работает для нас очень хорошо.

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

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[NewValue] [varchar](5000) NULL

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

12 голосов
/ 23 сентября 2010

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

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

Не пытайтесь использовать XML для этого, хранилище XML намного менее эффективно, чем хранилище таблицы собственных баз данных, которое использует этот тип триггера.

7 голосов
/ 02 сентября 2008

Рамеш, я занимался разработкой системы на основе первого подхода.
Оказалось, что хранение ревизий в формате XML ведет к огромному росту базы данных и значительно замедляет процесс.
Мой подход состоит в том, чтобы иметь одну таблицу для каждой сущности:

Employee (Id, Name, ... , IsActive)  

, где IsActive - знак последней версии

Если вы хотите связать некоторую дополнительную информацию с ревизиями, вы можете создать отдельную таблицу содержащий эту информацию и связать ее с таблицами сущностей, используя отношение PK \ FK.

Таким образом, вы можете хранить все версии сотрудников в одной таблице. Плюсы этого подхода:

  • Простая структура базы данных
  • Нет конфликтов, поскольку таблица становится доступной только для добавления
  • Вы можете вернуться к предыдущей версии, просто изменив флаг IsActive
  • Нет необходимости в объединениях для получения истории объекта

Обратите внимание, что первичный ключ должен быть не уникальным.

6 голосов
/ 02 сентября 2008

То, как я видел это в прошлом, это

Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );

Вы никогда не «обновляете» эту таблицу (кроме как для изменения действительного значения isCurrent), просто вставляете новые строки. Для любого заданного EmployeeId только 1 строка может иметь isCurrent == 1.

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

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

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

Это также создает очень небольшие накладные расходы для базы данных и приложения, особенно при выполнении запросов на чтение, что, вероятно, будет то, что вы будете делать в 99% случаев.

Также было бы довольно легко автоматически создавать таблицы истории и поддерживаемые триггеры (при условии, что это будет сделано через триггеры).

4 голосов
/ 11 июня 2013

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

enter image description here

В этом примере у нас есть сущность с именем employee . user таблица содержит записи ваших пользователей, а entity и entity_revision - это две таблицы, в которых хранится история изменений для всех типов сущностей, которые будут у вас в системе. Вот как работает этот дизайн:

Два поля entity_id и revision_id

Каждая сущность в вашей системе будет иметь уникальный идентификатор сущности. Ваша сущность может пройти ревизию, но ее entity_id останется прежним. Вы должны сохранить этот идентификатор сущности в своей таблице сотрудников (как внешний ключ). Вам также следует сохранить тип вашей сущности в таблице сущность (например, «сотрудник»). Теперь, что касается revision_id, как видно из его названия, он отслеживает изменения вашей сущности. Лучший способ, который я нашел для этого, это использовать employee_id в качестве вашего revision_id. Это означает, что у вас будут повторяющиеся идентификаторы ревизий для разных типов сущностей, но это меня не касается (я не уверен в вашем случае). Единственное важное замечание: комбинация entity_id и revision_id должна быть уникальной.

В таблице entity_revision также есть поле state , в котором указано состояние ревизии. Он может иметь одно из трех состояний: latest, obsolete или deleted (если вы не полагаетесь на дату пересмотра, это значительно повышает ваши запросы).

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

ВСТАВКИ

Для каждого сотрудника , который вы хотите вставить в базу данных, вы также добавите запись в entity и entity_revision . Эти последние две записи помогут вам отслеживать, кем и когда запись была вставлена ​​в базу данных.

UPDATE

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

УДАЛЕНИЕ

Для удаления сотрудника в entity_revision вставляется запись с указанием удаления и делается.

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

[UPDATE]

Поддерживая разделы в новых версиях MySQL, я считаю, что мой дизайн также обладает одним из лучших показателей. Можно разделить таблицу entity с помощью поля type, а раздел entity_revision - с помощью поля state. Это значительно повысит количество запросов SELECT, а дизайн будет простым и понятным.

3 голосов
/ 31 декабря 2008

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

...