【问题标题】:Adding trigger to sql tables将触发器添加到 sql 表
【发布时间】:2015-01-27 09:03:33
【问题描述】:

我正在尝试为我的数据库中的 sql 表创建一个触发器,以跟踪任何表和任何字段的任何插入、更新和删除,这个主题对我来说是新的,我在网上搜索并找到了一篇非常有用的文章这样做,但是当我运行此查询时,我收到了几条错误消息。

消息 8197,级别 16,状态 4,程序 BahbyGrade_ChangeTracking,第 3 行 对象“BahbyGrade”不存在或对该操作无效。

消息 311,级别 16,状态 1,过程 dtproperties_ChangeTracking,第 69 行 不能在“已插入”和“已删除”表中使用 text、ntext 或 image 列。

消息 311,级别 16,状态 1,过程 dtproperties_ChangeTracking,第 71 行 不能在“已插入”和“已删除”表中使用 text、ntext 或 image 列。

消息 311,级别 16,状态 1,过程 OutSide_ChangeTracking,第 69 行 不能在“已插入”和“已删除”表中使用 text、ntext 或 image 列。

消息 311,级别 16,状态 1,过程 OutSide_ChangeTracking,第 71 行 不能在“已插入”和“已删除”表中使用 text、ntext 或 image 列。

消息 8197,级别 16,状态 4,程序 PersonnelNew_ChangeTracking,第 3 行 对象“PersonnelNew”不存在或对该操作无效。

USE pr1
GO

IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= 'Audit')

CREATE TABLE Audit
(
AuditID [int]IDENTITY(1,1) NOT NULL,
Type char(1), 
TableName varchar(128), 
PrimaryKeyField varchar(1000), 
PrimaryKeyValue varchar(1000), 
FieldName varchar(128), 
OldValue varchar(1000), 
NewValue varchar(1000), 
UpdateDate datetime DEFAULT (GetDate()), 
UserName varchar(128)
)
GO

DECLARE @sql varchar(8000), @TABLE_NAME sysname
SET NOCOUNT ON;
SELECT @TABLE_NAME = MIN(TABLE_NAME) 
FROM INFORMATION_SCHEMA.Tables 
WHERE 
TABLE_TYPE= 'BASE TABLE' 
AND TABLE_NAME!= 'sysdiagrams'
AND TABLE_NAME!= 'Audit'
WHILE @TABLE_NAME IS NOT NULL

BEGIN

EXEC('IF OBJECT_ID (''' + @TABLE_NAME+ '_ChangeTracking'', ''TR'') IS NOT NULL DROP TRIGGER ' + @TABLE_NAME+ '_ChangeTracking')

SELECT @sql = 

create trigger ' + @TABLE_NAME+ '_ChangeTracking on ' + @TABLE_NAME+ ' for insert, update, delete

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) ,
@PKFieldSelect varchar(1000),
@PKValueSelect varchar(1000)

select @TableName = ''' + @TABLE_NAME+ '''

-- date and user
select @UserName = system_user ,
@UpdateDate = convert(varchar(8), getdate(), 112) + '' '' + convert(varchar(12), getdate(), 114)

-- 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 fields select for insert
select @PKFieldSelect = coalesce(@PKFieldSelect+''+'','''') + '''''''' + 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

select @PKValueSelect = coalesce(@PKValueSelect+''+'','''') + ''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(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName
while @field < @maxfield
begin

select @field = min(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName and ORDINAL_POSITION > @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 ORDINAL_POSITION = @field

select @sql = ''insert Audit (Type, TableName, PrimaryKeyField, PrimaryKeyValue, FieldName, OldValue, NewValue, UpdateDate, UserName)''

select @sql = @sql + '' select '''''' + @Type + ''''''''

select @sql = @sql + '','''''' + @TableName + ''''''''

select @sql = @sql + '','' + @PKFieldSelect

select @sql = @sql + '','' + @PKValueSelect

select @sql = @sql + '','''''' + @fieldname + ''''''''

select @sql = @sql + '',convert(varchar(1000),d.'' + @fieldname + '')''

select @sql = @sql + '',convert(varchar(1000),i.'' + @fieldname + '')''

select @sql = @sql + '','''''' + @UpdateDate + ''''''''

select @sql = @sql + '','''''' + @UserName + ''''''''

select @sql = @sql + '' from #ins i full outer join #del d''

select @sql = @sql + @PKCols

select @sql = @sql + '' where i.'' + @fieldname + '' <> d.'' + @fieldname 

select @sql = @sql + '' or (i.'' + @fieldname + '' is null and  d.'' + @fieldname + '' is not null)'' 

select @sql = @sql + '' or (i.'' + @fieldname + '' is not null and  d.'' + @fieldname + '' is null)'' 

exec (@sql)

end

end

SELECT @sql

EXEC(@sql)

SELECT @TABLE_NAME= MIN(TABLE_NAME) FROM INFORMATION_SCHEMA.Tables 
WHERE TABLE_NAME> @TABLE_NAME
AND TABLE_TYPE= 'BASE TABLE' 
AND TABLE_NAME!= 'sysdiagrams'
AND TABLE_NAME!= 'Audit'
END

我怎样才能跳过某些专栏?

http://weblogs.asp.net/jongalloway/adding-simple-trigger-based-auditing-to-your-sql-server-database

【问题讨论】:

  • 首先,您根本不应该使用文本和文本字段。它们已被弃用,替换效果要好得多,并且可以进行审计。

标签: sql sql-server sql-server-2008 stored-procedures triggers


【解决方案1】:

几天前我遇到了类似的问题。我修改了最初在您上面发布的网站上找到的版本。

下面的代码允许您包含具有 text、ntext 或 image 列的表。但是有几件事要记住。

  1. 由于 mssql 的工作方式,您无法引用 text、ntext 或 image 列,除非在“INSTEAD OF”触发器中。为了解决这个问题,我只检查这些被禁止的列类型之一是否包含在更新中。我不知道它是否真的改变了,或者捕捉到了它的旧价值。因此,只要这些列之一包含在更新中,我就认为它已经改变了。
  2. 如果您进行任何架构更改,包含 text、ntext 或 image 列的表将需要更新其触发器。或者如果您将任何列更改为 text、ntext 或 image 数据类型,则表也需要更新其触发器。

但是,如果你有 2008 或更高版本,可能建议使用SQL's built in change tracking

--Author: Rickac
--Date: 2015.03.23
--purpose: track changes to a database
--this has been modified but was originally based on http://weblogs.asp.net/jongalloway/adding-simple-trigger-based-auditing-to-your-sql-server-database
--note: this cannot properly detect changes to text,ntext or image columns.  it will track any update that includes one of these columns even if no change was made.
--      tables with text, ntext or image columns will need their trigger updated if a column is added to or removed from that table or in cases where a data type changes either to or from some other type to a text, ntext, or image type.

USE MY_DB -- TODO change this to your DB
GO

--hold some global script variables.
create table #vars (name sysname, value varchar(8000))
insert #vars values ('AUDIT_TABLE_NAME','Audit') -- this is the name you want the audit table to be.
insert #vars values ('TRIGGER_SUFFIX','_ChangeTracking') -- this is used for naming the trigger

Declare @sql varchar(max),@AUDIT_TABLE_NAME sysname
--retrieve global variable
select @AUDIT_TABLE_NAME=value from #vars where name='AUDIT_TABLE_NAME'

IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= @AUDIT_TABLE_NAME)
set @sql='CREATE TABLE '+ @AUDIT_TABLE_NAME +'
(
AuditID int IDENTITY(1,1) NOT NULL,
Type char(1),
TableName varchar(128),
PrimaryKeyField varchar(1000),
PrimaryKeyValue varchar(1000),
FieldName varchar(128),
OldValue varchar(1000),
NewValue varchar(1000),
UpdateDate datetime DEFAULT (GetDate()),
UserName varchar(128),
query varchar(4000)
)'
exec(@sql)
GO

DECLARE @sql varchar(max), @TABLE_NAME sysname, @AUDIT_TABLE_NAME sysname, @TRIGGER_SUFFIX varchar(255)
SET NOCOUNT ON
--retrieve global variable
select @AUDIT_TABLE_NAME=value from #vars where name='AUDIT_TABLE_NAME'
select @TRIGGER_SUFFIX=value from #vars where name='TRIGGER_SUFFIX'


DECLARE table_cursor CURSOR FOR 
  SELECT TABLE_NAME
    FROM INFORMATION_SCHEMA.Tables
    WHERE
      TABLE_TYPE= 'BASE TABLE'
      AND TABLE_NAME not in ('sysdiagrams',@AUDIT_TABLE_NAME)
      --use the below clause if you only want a specific table or list of tables
      -- AND TABLE_NAME in ('mytable')
    ORDER BY TABLE_NAME

OPEN table_cursor

FETCH NEXT FROM table_cursor INTO @TABLE_NAME

WHILE @@FETCH_STATUS = 0
BEGIN
print 'adding '+@TABLE_NAME+ @TRIGGER_SUFFIX 
EXEC('IF OBJECT_ID (''' + @TABLE_NAME+ @TRIGGER_SUFFIX +''', ''TR'') IS NOT NULL DROP TRIGGER ' + @TABLE_NAME+ @TRIGGER_SUFFIX)
set @sql =
'
create trigger ' + @TABLE_NAME+ @TRIGGER_SUFFIX +' on [' + @TABLE_NAME+ '] for insert, update, delete
as
set nocount on

declare @bit int ,
@field int ,
@maxfield int ,
@char int ,
@fieldname varchar(128) ,
@TableName varchar(128) ,
@PKCols varchar(1000) ,
@sql varchar(MAX),
@UpdateDate varchar(21) ,
@UserName varchar(128) ,
@Type char(1) ,
@PKFieldSelect varchar(1000),
@PKValueSelect varchar(1000),
@changes int


select @TableName = ''' + @TABLE_NAME+ '''
-- date and user
select @UserName = system_user ,
@UpdateDate = convert(varchar(8), getdate(), 112) + '' '' + convert(varchar(12), getdate(), 114)
-- 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
'
declare @contains_forbidden_columns int
set @contains_forbidden_columns=0
if exists(select * from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TABLE_NAME and DATA_TYPE in ('text','ntext','image'))
BEGIN
--darn, they have a text, ntext, or image column that means our job just got difficult
--lets try to list the columns individually.
set @contains_forbidden_columns=1

--get the primary keys so we can run a join
DECLARE @PKCols varchar(1000), @ColumnList varchar(max)
--reset our vars
set @ColumnList=NULL
set @PKCols=NULL

-- Get primary key columns for join
select @PKCols = coalesce(@PKCols + ' and', '') + ' t.[' + c.COLUMN_NAME + '] = c.[' + c.COLUMN_NAME +']'
from INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
where pk.TABLE_NAME = @TABLE_NAME
and CONSTRAINT_TYPE = 'PRIMARY KEY'
and c.TABLE_NAME = pk.TABLE_NAME
and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

--get the column list (excluding any text,ntext, or image columns)
select @ColumnList = coalesce(@ColumnList + ',', '') + ' c.['+ c.COLUMN_NAME +']'
 from INFORMATION_SCHEMA.COLUMNS c where TABLE_NAME = @TABLE_NAME and DATA_TYPE not in ('text','ntext','image')

select @ColumnList = coalesce(@ColumnList + ',', '') + ' t.['+ c.COLUMN_NAME +']'
 from INFORMATION_SCHEMA.COLUMNS c where TABLE_NAME = @TABLE_NAME and DATA_TYPE in ('text','ntext','image')

set @sql=@sql+
'
select '+@ColumnList+' into #ins from ['+@TABLE_NAME+'] t join inserted c on ('+@PKCols+')
select '+@ColumnList+' into #del from ['+@TABLE_NAME+'] t join deleted c on ('+@PKCols+')
'

END
ELSE
BEGIN

set @sql=@sql+
'
select * into #ins from inserted
select * into #del from deleted
'

END

set @sql=@sql+
'
CREATE TABLE #inputbuffer 
 (
  EventType nvarchar(30), 
  Parameters int, 
  EventInfo nvarchar(4000)
 )

 INSERT INTO #inputbuffer EXEC (''DBCC INPUTBUFFER(''+@@SPID+'') with NO_INFOMSGS'')

-- 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 fields select for insert
select @PKFieldSelect = coalesce(@PKFieldSelect+''+'','''') + '''''''' + 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
select @PKValueSelect = coalesce(@PKValueSelect+''+'','''') + ''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(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName
while @field < @maxfield
begin
select @field = min(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @TableName and ORDINAL_POSITION > @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 ORDINAL_POSITION = @field
select @sql = ''insert '+@AUDIT_TABLE_NAME+' (Type, TableName, PrimaryKeyField, PrimaryKeyValue, FieldName, OldValue, NewValue, UpdateDate, UserName, query)''
select @sql = @sql + '' select '''''' + @Type + ''''''''
select @sql = @sql + '','''''' + @TableName + ''''''''
select @sql = @sql + '','' + @PKFieldSelect
select @sql = @sql + '','' + @PKValueSelect
select @sql = @sql + '','''''' + @fieldname + ''''''''
select @sql = @sql + '',convert(varchar(1000),d.['' + @fieldname + ''])''
select @sql = @sql + '',convert(varchar(1000),i.['' + @fieldname + ''])''
select @sql = @sql + '','''''' + @UpdateDate + ''''''''
select @sql = @sql + '','''''' + @UserName + ''''''''
select @sql = @sql + '',(SELECT top 1 EventInfo FROM #inputbuffer)''
select @sql = @sql + '' from #ins i full outer join #del d''
select @sql = @sql + @PKCols
'
declare @where varchar(1000)
set @where=
'
  select @sql = @sql + '' where ''
  select @sql = @sql + ''i.['' + @fieldname + ''] <> d.['' + @fieldname + '']''
  select @sql = @sql + '' or (i.['' + @fieldname + ''] is null and d.['' + @fieldname + ''] is not null)''
  select @sql = @sql + '' or (i.['' + @fieldname + ''] is not null and d.['' + @fieldname + ''] is null)''
'

--does this table contain forbidden columns?
if @contains_forbidden_columns>0
BEGIN
--yes this table contains forbidden columns so we need to adjust the trigger accordingly and detect if this particular column is a forbidden type
set @sql=@sql+
'
-- see if this column contains a forbidden value
if not exists(select ORDINAL_POSITION from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = '''+@TABLE_NAME+''' and DATA_TYPE in (''text'',''ntext'',''image'') and sys.fn_IsBitSetInBitmask(COLUMNS_UPDATED(),ORDINAL_POSITION)!=0 and COLUMN_NAME=@fieldname)
BEGIN
  -- it does not contain a forbidden value so add the where clause for a clean column
'
+@where+
'
END
'
END
ELSE
BEGIN
--this table does not contain forbidden columns
set @sql=@sql+@where

END

set @sql=@sql+
'
exec (@sql)
end
end
'

SELECT @TABLE_NAME as 'table',@sql as 'trigger'
EXEC(@sql)

FETCH NEXT FROM table_cursor INTO @TABLE_NAME

END 

--******************************************
--BEGIN cleanup
CLOSE table_cursor;
DEALLOCATE table_cursor;

IF OBJECT_ID('tempdb..#vars') IS NOT NULL
BEGIN
  Drop table #vars
END
--END cleanup
--******************************************

【讨论】:

  • 变更跟踪是一种糟糕的审计方式,您无法捕获其他信息,例如谁做了变更。
  • 我同意,这可能也不是最好的。但它满足了我对逆向工程应用程序的需求,我无法访问这些应用程序的代码,但需要对数据库进行一些大规模更新以解决供应商不支持的问题。
  • 我并不是说它有时没用,只是说它对于审计来说是个糟糕的选择,尤其是当您有法律需要某些信息和保留时。
  • 此外,更改跟踪要求您的兼容模式不得低于 90,如果您实际使用已弃用的数据类型,则您可能处于较低的模式。
【解决方案2】:

我过去为解决无法引用 text 和 ntext 的事实所做的只是简单地加入原始表以获取这些值。假设您有触发器的表称为 mytable,您可以通过这种方式查看文本字段值并使用该技术将您的插入写入审计表:

SELECT i.id, t.mytextfield
from inserted i
join mytable t on t.id = i.id

很遗憾,您将无法获得旧值,而只能获得新值。但是,如果您让审计随着时间的推移运行,您应该能够找到在该更改之前存储的最后一个值(如果需要)。您可以通过将该数据手动复制到审计表来启动该过程,这样您就可以在审计开始时获得这些值。或者我想你可以有一个捕获这些字段之前值的前触发器和捕获新值的后触发器。 (我没试过)

但最好的办法是咬紧牙关,尽一切努力摆脱这些数据类型。它们已被弃用。 Varchar(max) 和 nvarchar(max) 无论如何都要好得多。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多