Вопрос

У нас есть требование в проекте хранить все ревизии (историю изменений) для объектов в базе данных.В настоящее время у нас есть 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, .., ..)"

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

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

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

Это было полезно?

Решение

  1. Делать нет поместите все это в одну таблицу с атрибутом дискриминатора IsCurrent.Это просто вызывает проблемы в дальнейшем, требует суррогатных ключей и множества других проблем.
  2. В проекте 2 есть проблемы с изменениями схемы.Если вы измените таблицу «Сотрудники», вам придется изменить таблицу «Истории сотрудников» и все связанные с ней процедуры.Потенциально удваивает усилия по изменению схемы.
  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 

Другие советы

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

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

Создайте таблицу под названием «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 для автоматического создания триггеров на основе определений таблиц.

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

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

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

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

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

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

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

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

Единственное отличие состоит в том, что мы сохраняем только новое значение.Старое значение все-таки сохраняется в предыдущей строке истории.

[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 столбцами.Таким образом, вам нужно будет сохранить только тот столбец, который изменился, вместо того, чтобы хранить всю строку.

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

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

Примечания

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

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

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

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

insert into EmployeeHistory select * from Employe where ID = XX

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

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

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

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

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

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

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

где Активен это признак последней версии

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

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

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

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

Раньше я видел, как это делалось,

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

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

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

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

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

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

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

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

enter image description here

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

Эти две области entity_id (идентификатор объекта) и revision_id ( идентификатор ревизии )

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

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

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

Вставка

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

Обновить

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

УДАЛЕНИЕ

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

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

[ОБНОВЛЕНИЕ]

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

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

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

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

Если вы хотите сделать первый вариант, возможно, вы захотите использовать XML и для таблицы «Сотрудники».Большинство новых баз данных позволяют выполнять запросы к полям XML, поэтому это не всегда проблема.И, возможно, было бы проще иметь один способ доступа к данным сотрудников, независимо от того, последняя это версия или более ранняя.

Хотя я бы попробовал второй подход.Вы можете упростить это, имея только одну таблицу «Сотрудники» с полем DateModified.Сотрудник DateModified будет первичным ключом, и вы можете сохранить новую редакцию, просто добавив строку.Таким образом, архивирование старых версий и восстановление версий из архива также упрощается.

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

Как насчет:

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

Вы создаете первичный ключ (EmployeeId, DateModified) и для получения «текущих» записей просто выбираете MAX(DateModified) для каждого идентификатора сотрудника.Хранение IsCurrent — очень плохая идея, потому что, во-первых, его можно вычислить, а во-вторых, данные слишком легко могут рассинхронизироваться.

Вы также можете создать представление, в котором будут перечислены только самые последние записи, и в основном использовать его при работе в приложении.Преимущество этого подхода в том, что у вас нет дубликатов данных, и вам не нужно собирать данные из двух разных мест (текущие в «Сотрудниках» и заархивированные в «Истории сотрудников»), чтобы получить всю историю или откат и т. д.) .

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

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

// Holds the Employee revisions in rows.
"EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"

Или глобальное решение для применения:

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

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"

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

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

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"

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

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

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

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

Похоже, вы хотите отслеживать изменения в определенных объектах с течением времени, напримерID 3, "bob", "123 main street", затем еще один ID 3, "bob" "234 Elm St" и так далее, по сути, имея возможность выблевать историю изменений, показывающую каждый адрес, по которому "bob" был .

Лучший способ сделать это — иметь поле «текущее» для каждой записи и (вероятно) метку времени или FK для таблицы даты/времени.

Затем вставки должны установить значение «является текущим», а также отменить значение «является текущим» в предыдущей записи «является текущим».В запросах необходимо указывать «актуально», если вам не нужна вся история.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top