Мы пытаемся отследить изменения записей в таблице SQL Server. Пользователи жалуются, что они изменяют запись утром, а когда они возвращаются днем, значение изменяется обратно.
Пример: значение столбца было нулевым. Пользователь изменил значение, например 6. Когда он вернется к записи, значение может снова быть нулевым или нулевым.
Мы включили аудит в SQL Server, но я не вижу первого редактирования (утра) или возврата обратно к нулю или нулю. Да, я думаю, что ошибка пользователя, но прежде чем обвинять пользователя, я должен устранить все другие возможности. Я вытащил из сети код события триггера и изменил его для своих целей. Я положил его на конкретную таблицу под триггерами. Это тянет записи, как я ожидал. Он отслеживает, какие столбцы были изменены, старые и новые значения. Я признаю, что я не знаком со всем, что делает этот код, поэтому я хочу спросить, есть ли в этом коде потенциальные дыры.
Пример: не будет ли записано событие, если кто-то из-за NULL изменил столбец с 1 на NULL? ModifiedOn
и ModifiedBy
должны быть автоматически заполнены, когда программа сохраняет запись. Триггер захватит ModifiedOn
, но не всегда ModifiedBy
. Был задан вопрос, если ModifiedBy
совпадает старое значение и новое значение (один и тот же человек внес оба изменения), не будет ли запись в журнале ModifiedBy
, поскольку значения совпадают.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[TR_AppointmentBase_LOG]
ON [dbo].[AppointmentBase]
FOR UPDATE
AS
DECLARE @bit INT,
@field INT,
@maxfield INT,
@char INT,
@fieldname VARCHAR(128),
@TableName VARCHAR(128),
@PKCols VARCHAR(1000),
@sql VARCHAR(2000),
@UpdateDate VARCHAR(21),
@UserName VARCHAR(128),
@Type CHAR(1),
@PKSelect VARCHAR(1000),
@PKSelect2 VARCHAR(1000)
--You will need to change @TableName to match the table to be audited.
-- Here we made GUESTS for your example.
SELECT @TableName = 'AppointmentBase'
SELECT @UserName = SYSTEM_USER,
@UpdateDate = CONVERT(NVARCHAR(30), GETDATE(), 126)
-- Action
IF EXISTS (
SELECT *
FROM INSERTED
)
IF EXISTS (
SELECT *
FROM DELETED
)
SELECT @Type = 'U'
ELSE
SELECT @Type = 'I'
ELSE
SELECT @Type = 'D'
-- get list of columns
SELECT * INTO #ins
FROM INSERTED
SELECT * INTO #del
FROM DELETED
-- Get primary key columns for full outer join
SELECT @PKCols = COALESCE(@PKCols + ' and', ' on')
+ ' i.[' + c.COLUMN_NAME + '] = d.[' + c.COLUMN_NAME + ']'
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk,
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
WHERE pk.TABLE_NAME = @TableName
AND CONSTRAINT_TYPE = 'PRIMARY KEY'
AND c.TABLE_NAME = pk.TABLE_NAME
AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
-- Get primary key select for insert
SELECT @PKSelect = COALESCE(@PKSelect + '+', '')
+ '''<[' + COLUMN_NAME
+ ']=''+convert(varchar(100),
coalesce(i.[' + COLUMN_NAME + '],d.[' + COLUMN_NAME + ']))+''>''',
@PKSelect2 = COALESCE(@PKSelect2 + '+', '')
+ '+convert(varchar(100), coalesce(i.[' + COLUMN_NAME + '],d.[' + COLUMN_NAME + ']))'
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk,
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
WHERE pk.TABLE_NAME = @TableName
AND CONSTRAINT_TYPE = 'PRIMARY KEY'
AND c.TABLE_NAME = pk.TABLE_NAME
AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
IF @PKCols IS NULL
BEGIN
RAISERROR('no PK on table %s', 16, -1, @TableName)
RETURN
END
SELECT @field = 0,
-- @maxfield = MAX(COLUMN_NAME)
@maxfield = -- FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName
MAX(
COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
COLUMN_NAME,
'ColumnID'
)
)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
WHILE @field < @maxfield
BEGIN
SELECT @field = MIN(
COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
COLUMN_NAME,
'ColumnID'
)
)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
AND COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
COLUMN_NAME,
'ColumnID'
) > @field
SELECT @bit = (@field - 1)% 8 + 1
SELECT @bit = POWER(2, @bit - 1)
SELECT @char = ((@field - 1) / 8) + 1
IF SUBSTRING(COLUMNS_UPDATED(), @char, 1) & @bit > 0
OR @Type IN ('I', 'D')
BEGIN
SELECT @fieldname = COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
AND COLUMNPROPERTY(
OBJECT_ID(TABLE_SCHEMA + '.' + @TableName),
COLUMN_NAME,
'ColumnID'
) = @field
SELECT @sql =
'
insert into AppointmentBase_Log2 ( Type,
TableName,
PK,
FieldName,
OldValue,
NewValue,
UpdateDate,
UserName)
select ''' + @Type + ''','''
+ @TableName + ''',' + @PKSelect2
+ ',''' + @fieldname + ''''
+ ',convert(varchar(1000),d.' + @fieldname + ')'
+ ',convert(varchar(1000),i.' + @fieldname + ')'
+ ',''' + @UpdateDate + ''''
+ ',''' + @UserName + ''''
+ ' from #ins i full outer join #del d'
+ @PKCols
+ ' where i.' + @fieldname + ' <> d.' + @fieldname
+ ' or (i.' + @fieldname + ' is null and d.'
+ @fieldname
+ ' is not null)'
+ ' or (i.' + @fieldname + ' is not null and d.'
+ @fieldname
+ ' is null)'
EXEC (@sql)
END
END
Пользователи и менеджеры думают, что я не нахожу все изменения в таблице с моим кодом. В основном потому, что я не могу доказать, что они изменили запись в первую очередь, не говоря уже о втором изменении. Исходя из моего ограниченного знания описанного выше процесса, я не достаточно уверен, чтобы сказать, что они ошибаются.
Существует ли транзакция, которая может обновить запись в таблице, которую этот код может пропустить? Скажем, запланированное задание обслуживания, которое может не вызывать типы «U», «D» или «I». Или, может быть, пакетная инъекция из внешнего источника?
Или, как вы думаете, я хорошо справляюсь с вышесказанным и должен рассматривать пользовательский или графический интерфейс как не завершивший транзакцию.