Шаблоны для реализации отслеживания изменений поля - PullRequest
6 голосов
/ 21 апреля 2010

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

В базе данных я реализовал это как одну таблицу FieldChanges со следующими полями:

  • TableName
  • FieldName
  • RecordId
  • DateOfChange
  • ChangedBy
  • IntValue
  • TextValue
  • DateTimeValue
  • BoolValue

Функция сохранения изменений в объекте определяет для каждого поля, было ли оно изменено, и вставляет запись в FieldChanges, если она имеет: если тип измененного поля int, он записывает его в поле IntValue в таблице FieldChanges и т. д.

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

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

Мне было бы очень интересно - спасибо.

David

Ответы [ 4 ]

6 голосов
/ 09 июня 2011

Триггеры.

Мы написали графический интерфейс пользователя (внутренне называемый Red Matrix Reloaded ), чтобы упростить создание / управление триггерами ведения журнала аудита.

Вот некоторые DDL используемых материалов:


Таблица AuditLog

CREATE TABLE [AuditLog] (
    [AuditLogID] [int] IDENTITY (1, 1) NOT NULL ,
    [ChangeDate] [datetime] NOT NULL CONSTRAINT [DF_AuditLog_ChangeDate] DEFAULT (getdate()),
    [RowGUID] [uniqueidentifier] NOT NULL ,
    [ChangeType] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
    [TableName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
    [FieldName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
    [OldValue] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
    [NewValue] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
    [Username] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
    [Hostname] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
    [AppName] [varchar] (128) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
    [UserGUID] [uniqueidentifier] NULL ,
    [TagGUID] [uniqueidentifier] NULL ,
    [Tag] [varchar] (8000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL 
)

Запуск журнала для вставок

CREATE TRIGGER LogInsert_Nodes ON dbo.Nodes
FOR INSERT
AS

/* Load the saved context info UserGUID */
DECLARE @SavedUserGUID uniqueidentifier

SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier)
FROM master.dbo.sysprocesses
WHERE spid = @@SPID

DECLARE @NullGUID uniqueidentifier
SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}'

IF @SavedUserGUID = @NullGUID
BEGIN
    SET @SavedUserGUID = NULL
END

    /*We dont' log individual field changes Old/New because the row is new.
    So we only have one record - INSERTED*/

    INSERT INTO AuditLog(
            ChangeDate, RowGUID, ChangeType, 
            Username, HostName, AppName,
            UserGUID, 
            TableName, FieldName, 
            TagGUID, Tag, 
            OldValue, NewValue)

    SELECT
        getdate(), --ChangeDate
        i.NodeGUID, --RowGUID
        'INSERTED', --ChangeType
        USER_NAME(), HOST_NAME(), APP_NAME(), 
        @SavedUserGUID, --UserGUID
        'Nodes', --TableName
        '', --FieldName
        i.ParentNodeGUID, --TagGUID
        i.Caption, --Tag
        null, --OldValue
        null --NewValue
    FROM Inserted i

Запуск регистрации обновлений

CREATE TRIGGER LogUpdate_Nodes ON dbo.Nodes
FOR UPDATE AS

/* Load the saved context info UserGUID */
DECLARE @SavedUserGUID uniqueidentifier

SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier)
FROM master.dbo.sysprocesses
WHERE spid = @@SPID

DECLARE @NullGUID uniqueidentifier
SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}'

IF @SavedUserGUID = @NullGUID
BEGIN
    SET @SavedUserGUID = NULL
END

    /* ParentNodeGUID uniqueidentifier */
    IF UPDATE (ParentNodeGUID)
    BEGIN
        INSERT INTO AuditLog(
            ChangeDate, RowGUID, ChangeType, 
            Username, HostName, AppName,
            UserGUID, 
            TableName, FieldName, 
            TagGUID, Tag, 
            OldValue, NewValue)
        SELECT 
            getdate(), --ChangeDate
            i.NodeGUID, --RowGUID
            'UPDATED', --ChangeType
            USER_NAME(), HOST_NAME(), APP_NAME(), 
            @SavedUserGUID, --UserGUID
            'Nodes', --TableName
            'ParentNodeGUID', --FieldName
            i.ParentNodeGUID, --TagGUID
            i.Caption, --Tag
            d.ParentNodeGUID, --OldValue
            i.ParentNodeGUID --NewValue
        FROM Inserted i
            INNER JOIN Deleted d
            ON i.NodeGUID = d.NodeGUID
        WHERE (d.ParentNodeGUID IS NULL AND i.ParentNodeGUID IS NOT NULL)
        OR (d.ParentNodeGUID IS NOT NULL AND i.ParentNodeGUID IS NULL)
        OR (d.ParentNodeGUID <> i.ParentNodeGUID)
    END

    /* Caption varchar(255) */
    IF UPDATE (Caption)
    BEGIN
        INSERT INTO AuditLog(
            ChangeDate, RowGUID, ChangeType, 
            Username, HostName, AppName,
            UserGUID, 
            TableName, FieldName, 
            TagGUID, Tag, 
            OldValue, NewValue)
        SELECT 
            getdate(), --ChangeDate
            i.NodeGUID, --RowGUID
            'UPDATED', --ChangeType
            USER_NAME(), HOST_NAME(), APP_NAME(), 
            @SavedUserGUID, --UserGUID
            'Nodes', --TableName
            'Caption', --FieldName
            i.ParentNodeGUID, --TagGUID
            i.Caption, --Tag
            d.Caption, --OldValue
            i.Caption --NewValue
        FROM Inserted i
            INNER JOIN Deleted d
            ON i.NodeGUID = d.NodeGUID
        WHERE (d.Caption IS NULL AND i.Caption IS NOT NULL)
        OR (d.Caption IS NOT NULL AND i.Caption IS NULL)
        OR (d.Caption <> i.Caption)
    END

...

/* ImageGUID uniqueidentifier */
IF UPDATE (ImageGUID)
BEGIN
    INSERT INTO AuditLog(
        ChangeDate, RowGUID, ChangeType, 
        Username, HostName, AppName,
        UserGUID, 
        TableName, FieldName, 
        TagGUID, Tag, 
        OldValue, NewValue)
    SELECT 
        getdate(), --ChangeDate
        i.NodeGUID, --RowGUID
        'UPDATED', --ChangeType
        USER_NAME(), HOST_NAME(), APP_NAME(), 
        @SavedUserGUID, --UserGUID
        'Nodes', --TableName
        'ImageGUID', --FieldName
        i.ParentNodeGUID, --TagGUID
        i.Caption, --Tag
        (SELECT Caption FROM Nodes WHERE NodeGUID = d.ImageGUID), --OldValue
        (SELECT Caption FROM Nodes WHERE NodeGUID = i.ImageGUID) --New Value
    FROM Inserted i
        INNER JOIN Deleted d
        ON i.NodeGUID = d.NodeGUID
    WHERE (d.ImageGUID IS NULL AND i.ImageGUID IS NOT NULL)
    OR (d.ImageGUID IS NOT NULL AND i.ImageGUID IS NULL)
    OR (d.ImageGUID <> i.ImageGUID)
END

Триггер для входа в систему Удалить

CREATE TRIGGER LogDelete_Nodes ON dbo.Nodes
FOR DELETE
AS

/* Load the saved context info UserGUID */
DECLARE @SavedUserGUID uniqueidentifier

SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier)
FROM master.dbo.sysprocesses
WHERE spid = @@SPID

DECLARE @NullGUID uniqueidentifier
SELECT @NullGUID = '{00000000-0000-0000-0000-000000000000}'

IF @SavedUserGUID = @NullGUID
BEGIN
    SET @SavedUserGUID = NULL
END

    /*We dont' log individual field changes Old/New because the row is new.
    So we only have one record - DELETED*/

    INSERT INTO AuditLog(
            ChangeDate, RowGUID, ChangeType, 
            Username, HostName, AppName,
            UserGUID, 
            TableName, FieldName, 
            TagGUID, Tag, 
            OldValue,NewValue)

    SELECT
        getdate(), --ChangeDate
        d.NodeGUID, --RowGUID
        'DELETED', --ChangeType
        USER_NAME(), HOST_NAME(), APP_NAME(), 
        @SavedUserGUID, --UserGUID
        'Nodes', --TableName
        '', --FieldName
        d.ParentNodeGUID, --TagGUID
        d.Caption, --Tag
        null, --OldValue
        null --NewValue
    FROM Deleted d

И чтобы узнать, какой пользователь в программном обеспечении выполнил обновление, каждое соединение «регистрируется на SQL Server», вызывая хранимую процедуру:

CREATE PROCEDURE dbo.SaveContextUserGUID @UserGUID uniqueidentifier AS

/* Saves the given UserGUID as the session's "Context Information" */
IF @UserGUID IS NULL
BEGIN
    PRINT 'Emptying CONTEXT_INFO because of null @UserGUID'
    DECLARE @BinVar varbinary(128)
    SET @BinVar = CAST( REPLICATE( 0x00, 128 ) AS varbinary(128) )
    SET CONTEXT_INFO @BinVar
    RETURN 0
END

DECLARE @UserGUIDBinary binary(16) --a guid is 16 bytes
SELECT @UserGUIDBinary = CAST(@UserGUID as binary(16))
SET CONTEXT_INFO @UserGUIDBinary


/* To load the guid back 
DECLARE @SavedUserGUID uniqueidentifier

SELECT @SavedUserGUID = CAST(context_info as uniqueidentifier)
FROM master.dbo.sysprocesses
WHERE spid = @@SPID

select @SavedUserGUID AS UserGUID
*/

Примечания

  • Формат кода Stackoverflow удаляет большинство пустых строк - поэтому форматирование отстой
  • Мы используем таблицу пользователей, без встроенной защиты
  • Этот код предоставлен для удобства - никакая критика нашего выбора дизайна не допускается.Пуристы могут настаивать на том, что весь код ведения журнала должен выполняться на бизнес-уровне - они могут прийти сюда и написать / поддержать его для нас.
  • BLOB-объекты не могут быть зарегистрированы с помощью триггеров в SQL Server (нет версии «до»)из блога - есть только то, что есть).Text и nText являются BLOB-объектами, что делает записи либо не блокируемыми, либо делает их varchar (2000).
  • столбец Tag используется в качестве произвольного текста для идентификации строки (например, если клиент был удален,тег будет показывать «General Motors North America» в таблице журнала аудита.
  • TagGUID используется для указания на «родителя» строки. Например, регистрация InvoiceLineItems указывает на InvoiceHeader . Таким образом, любой, кто ищет записи журнала аудита, относящиеся к конкретной накладной, найдет удаленные «позиции» по TagGUID позиции в журнале аудита.
  • иногда «OldValue»и значения «NewValue» записываются в виде дополнительного выбора - для получения значимой строки, т. е. «

    OldValue: {233d-ad34234 ..} NewValue: {883-sdf34 ...}

менее полезен в журнале аудита, чем:

OldValue: Daimler Chrysler
NewValue: Cerberus Capital Management

Последнее замечание : не стесняйтесь не делать то, что мы делаем. Это здорово для нас,но все остальные могут не использовать его.

1 голос
/ 23 апреля 2010

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

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

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

Моя организация использует это только для следующих целей:

  • Аудит для dbas и поддержки для ручного определения того, что произошло (с использованием SQL).
  • Enterprise Data Warehouse (SAS) отбирает все дельты из производственных систем для анализа.

Мы создаем разные таблицы, если они необходимы для самих операционных систем.

0 голосов
/ 29 апреля 2010

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

0 голосов
/ 21 апреля 2010

Я решаю это путем управления версиями. Одна версия - одна строка таблицы. Последняя версия - строка с самой большой датой последнего обновления.

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