Service broker + trigger based data auditing
I was assigned task to build our own Data Audit mechanism in SQL Server 2005 last year. There were number of articles and scripts on the internet about asynchronous auditing with service broker. I decided to get the best out of those articles and scripts and create my own trigger + service broker based solution.
Each table assigned for auditing on production database has AFTER INSERT, UPDATE, DELETE trigger. This trigger converts the record(s) of inserted and deleted tables into XML and a message dialog is initiated from the production database. This message is received by the audit database and parsed into specific tables. There will not (or hardly) be any performance issue because of asynchronous messaging between production and audit database.
The example XML is listed below.
<AuditMsg>
<SourceDb>ProductionDatabase</SourceDb>
<SourceTable>UserTable</SourceTable>
<UserId>UserID</UserId>
<AppUserId />
<DMLType>U</DMLType>
<ChangedData>
<t ID="8521" Name="Atif" Class="SQL Server" />
</ChangedData>
<NewRec>
<t ID="8521" Name="Atif Sheikh" Class="SQL Server" />
</NewRec>
</AuditMsg>
As you can see from the above XML, it has an element ChangedData which have another element t. t element have attributes depending upon the structure of the table (whose record is changed and the trigger sent the XML message via service broker) of the production database. Same is the case with the attribute NewRec. In the above XML, the UserTable generated this XML. You can see from the XML that the Name is changed from ‘Atif’ to ‘Atif Sheikh’. The XML also holds the information of the production database name and SQL Server UserID, who changed the data of the UserTable. This XML is parsed in audit database. The information in the attributes of ChangedData and NewRec elements is compared and placed in another table tblAudDetail for efficient queries and reports. The parsing of XML is discussed in detail later.
** **
Outline of the solution
The outline of the solution is;
1. Create New Audit Database
2. Activate Service Broker on New Audit Database and your production database
3. Create stored procedure to apply triggers on tables for auditing
I will discuss each point in detail.
1. Create New Audit Database
This database will hold new and old values of columns changed on the production database. Create a new database as AuditDatabase. It will have following Tables;
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[tblAud](
[tID] [int] IDENTITY(1,1) NOT NULL,
[xmlQuery] [xml] NULL,
[Upd_Det] [bit],
[tDate] [datetime] NULL CONSTRAINT [DF_tblAud_tDate] DEFAULT (getdate()),
CONSTRAINT [PK_tblAud] PRIMARY KEY CLUSTERED
(
[tID] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
Column xmlQuery of table tblAud will hold XML sent by the trigger of the production database table. Column upd_Det bit column will tell us if XML is parsed and information inserted in tblAudDetail. It will be 1 in case of XML is parsed and information is inserted in tblAudDetail. Column tDate will tell us the date when record was arrived in the tblAud. Considering the above XML example, tblAud will have data as,
Here is the structure of tblAudDetail;
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[tblAudDetail](
[AudDetID] [int] IDENTITY(1,1) NOT NULL,
[tid] [int] NULL,
[dbName] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[TABLENAME] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[FieldName] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Priorval] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[CurrVal] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_tblAudDetail] PRIMARY KEY CLUSTERED
(
[AudDetID] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
Table tblAudDetail will hold the information of the attributes of elements ChangedData and NewRec from XML in tblAud. You can see from the schema of tblAudDetail that it has columns dbName. It means we can use one audit database for multiple production databases. Considering the above mentioned XML, the tblAudDetail will have following records;
As you can see from the above dataset, only record number 2 have different PriorValue and CurrValue. Secondly, we also need the ID of the UserTable to recognize the record which in row number 1. We don’t need the record number 3. So, according to the code (mentioned ahead), the recordset will be;
One record of ID to recognize the record and the second record of the changed information of Column ‘Name’ with prior value of ‘Atif’ and new value of ‘Atif Sheikh’.
Here is the structure of tblErrorlog,
USE [AuditDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[tblErrorLog](
[ErrorNumber] [int] NOT NULL,
[ErrorSeverity] [int] NULL,
[ErrorState] [int] NULL,
[ErrorLine] [int] NULL,
[ErrorProcedure] [nvarchar](126) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[ErrorMessage] [nvarchar](4000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[ErrorTime] [datetime] NULL CONSTRAINT [DF__tblErrorL__Error__4865BE2A] DEFAULT (getdate()),
[DbUserName] [sysname] COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[sysUserName] [sysname] COLLATE SQL_Latin1_General_CP1_CI_AS NULL
) ON [PRIMARY]
Table tblErrorlog will hold details of errors in messages so that XML can be debugged.
The audit database will also have three stored procedures.
[dbo].[uspInsertAuditRec]
[dbo].[uspUpdateDetail]
[dbo].[uspUpdateAuditDetailFromAuditTableID]
Script for uspInsertAuditRec;
USE [AuditDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[uspInsertAuditRec]
AS
BEGIN
SET NOCOUNT ON;
Begin Try
Declare @ConversationHandle as uniqueidentifier
Declare @MessageBody as varbinary(max)
Declare @MessageXML as XML
Declare @MessageType as sysname
Declare @pBQuery nvarchar(max)
Begin TRANSACTION
Print 'Started Receiving';
WAITFOR(
RECEIVE top (1)
@MessageType = message_type_name,
@ConversationHandle = conversation_handle,
@MessageBody = message_body
FROM BLOB_AdtQueue_Remote1
), TIMEOUT 1000;
Select @pBQuery = convert(nvarchar(max),@MessageBody)
Select @MessageXML = cast(@pBQuery as XML)
Declare @sID as INT
Declare @DBName as nvarchar(100)
Declare @TableName as nvarchar(100)
IF @MessageType = 'BLOB'
BEGIN
select @DBName = x.header.value('(//SourceDb)[1]', 'nvarchar(50)') ,
@TableName = x.header.value('(//SourceTable)[1]', 'nvarchar(50)')
FROM @MessageXML.nodes('//AuditMsg') AS x(header)
Insert into tblAud (xmlQuery,upd_det) values (cast(@pBQuery as XML),0)
Select @sID = Scope_identity()
END
ELSE
BEGIN
INSERT INTO tblAud (xmlQuery,upd_det) values ('<a>aaa<a/>','<a>aaa<a/>')
END
END CONVERSATION @ConversationHandle
COMMIT
End Try
Begin Catch
Rollback
print 'In Catch...'
INSERT INTO DBO.tblErrorLog
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_STATE() AS ErrorState,
ERROR_LINE() AS ErrorLine,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_MESSAGE() AS ErrorMessage,GETDATE(),
CONVERT(sysname ,USER_NAME())AS DBUSERNAME,
CONVERT(SYSNAME,SUSER_SNAME()) AS SYSUSERNAME
Insert into dbo.tblErrorXML
Select @MessageBody
End Catch
END
The above stored procedure uspInsertAuditRec gets the message from the receiving queue in AuditDatabase. It checks the validity of the message and inserts it into tblAud for further processing. Only BLOB (the message type that we will create soon) message types are processed.
The following stored procedures will parse XML and will mark the record in tblAud;
USE [AuditDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Atif Sheikh
-- Create date: 24-May-2010
-- Description: Update Detail
-- =============================================
CREATE PROCEDURE [dbo].[uspUpdateDetail]
AS
BEGIN
SET NOCOUNT ON;
Declare @tid int
Declare @DbName varchar(100)
Declare @TableName varchar(100)
Declare d1 Cursor for Select tid from tblAud where upd_det = 0
Open d1
Fetch Next from d1 into @tid
while @@Fetch_Status = 0
begin
PRINT @tid
select @DbName = xmlQuery.value('(//SourceDb)[1]', 'varchar(50)') ,
@TableName = xmlQuery.value('(//SourceTable)[1]', 'varchar(50)')
FROM tblAud where tid = @tid
if @DbName is Not Null and @TableName is Not Null
exec uspUpdateAuditDetailFromAuditTableID @tid,@DbName,@TableName
Fetch Next from d1 into @tid
end
Deallocate d1
END
The above stored procedure gets record from tblAud that are not processes (Upd_Det = 0) and passes on to the stored procedure uspUpdateAuditDetailFromAuditTableID.
USE [AuditDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Atif-ullah Sheikh
-- Create date: 24-May-2008
-- Description: Insert into tblAudDetail
-- =============================================
CREATE PROCEDURE [dbo].[uspUpdateAuditDetailFromAuditTableID]
(
@pID int,
@pDBName nvarchar(100),
@pTableName nvarchar(100)
)
AS
BEGIN
SET NOCOUNT ON;
Begin Try
Begin Tran
Declare @ColName nvarchar(max)
Declare @Chk1 nvarchar(max)
Declare @Chk2 nvarchar(max)
Declare @sSql varchar(max)
Declare @SQL1 nvarchar(max)
Declare @SQL nvarchar(max)
Declare @pDef nvarchar(max)
Declare @sSql2 varchar(max)
Declare @SelectCols varchar(max)
Declare @SelectCols2 varchar(max)
Declare @ObjId int
Set @sql1 = N'Select @ObjId = Object_ID from ' + @pDBName + '.sys.objects where name = ''' + @pTableName + ''''
Set @pDef = N'@ObjId varchar(100) OUTPUT'
Exec sp_executesql @sql1 ,@pDef,@ObjId=@ObjId OUTPUT
Set @sql = 'Declare col2 Cursor for Select name from ' + @pDBName + '.sys.columns where Object_Id = '+ cast(@ObjID as varchar(10))+ ' and system_type_id not in (34,35,99,241,165,173)'
print(@sql)
Exec (@sql)
Open col2
Declare @XMLvar XML
Select @XMLvar = xmlQuery from tblAud where tid = @pID
Set @sql = ''
Declare @SQL2 varchar(max)
Declare @SQL3 varchar(max)
Declare @SQL4 varchar(max)
Set @sql2 = ''
Set @sql3 = ''
Set @sql4 = ''
Declare @cname varchar(100)
Fetch next From Col2 into @cname
Set @sql3 = '(Order By x.header.value(''@' + @cname + ''',''varchar(100)'')) '
Set @sql = 'Declare @XMLvar XML
Select @XMLvar = xmlQuery from tblAud where tid = ' + cast(@pID as varchar(10))+ '
Select * into ##ctbl from ( Select Row_Number() Over '
while @@Fetch_Status = 0
begin
if @Sql2 <> '' Set @Sql2 = @Sql2 + ','
Set @Sql2 = @Sql2 + 'x.header.value(''@' + @cname + ''', ''varchar(50)'') ' + @cname + '_d'
if @Sql4 <> '' Set @Sql4 = @Sql4 + ','
Set @Sql4 = @Sql4 + 'x.header.value(''@' + @cname + ''', ''varchar(50)'') ' + @cname + '_i'
Fetch next From Col2 into @cname
end
Set @sql = @sql + @sql3 + ' d_rid,' + @sql2
Set @sql = @sql + ' FROM @XMLvar.nodes(''//ChangedData/t'') AS x(header)) xx
Left OUter join (
select Row_number() over ' + @sql3 + ' i_rid,' + @sql4 + '
FROM @XMLvar.nodes(''//NewRec/t'') AS x(header))yy
on yy.i_rid = xx.d_rid '
print(@sql)
Deallocate Col2
Exec (@sql)
Declare @dval varchar(100)
Declare @ival varchar(100)
Declare @rid int
Set @sql = 'Declare col2 Cursor SCROLL for Select name from ' + @pDBName + '.sys.columns where Object_Id = '+ cast(@ObjID as varchar(10))+ ' and system_type_id not in (34,35,99,241,165,173)'
Exec (@sql)
Open col2
Declare @vRID int
Declare Col3 Cursor for Select Case when i_RID is Null then d_RID else i_RID end from ##ctbl
Open Col3
Fetch Next from col3 into @rid
while @@Fetch_Status = 0
begin
Fetch next From Col2 into @cname
While @@Fetch_Status = 0
begin
Set @sql1 = ''
Set @sql1 = @sql1 + N' Select @dval1 = '+ @cname + '_d ,@ival1 = '+ @cname +'_i from ##ctbl where case when i_RID is Null then d_rid else i_rid end = ' + cast(@rid as varchar(10))
Set @pDef = N'@dval1 varchar(100) OUTPUT, @ival1 varchar(100) OUTPUT'
Set @vRID = 0
Select @vRID = i_rid from ##ctbl where case when i_RID is Null then d_rid else i_rid end = @rid
Exec sp_executesql @sql1 ,@pDef,@dval1=@dval OUTPUT,@ival1=@ival OUTPUT
if (IsNull(@dval,'') <> IsNull(@ival,'')) or @cname Like '%ID'
begin
Insert into tblAudDetail (tid,DBName,TableName,FieldName,PriorVal,CurrVal)
Values (@pID,@pDBName,@pTableName,@cname,case when IsNull(@vRID,0) <> 0 then @dval else @ival end,case when IsNull(@vRID,0) <> 0 then @ival else @dval end)
end
Fetch next From Col2 into @cname
end
Fetch FIRST From Col2 into @cname
Fetch PRIOR From Col2 into @cname
Fetch Next from col3 into @rid
end
Deallocate Col2
Deallocate Col3
Drop table ##ctbl
update tblAud set upd_det = 1 where tid = @pID
Commit Tran
End Try
begin Catch
Rollback
INSERT INTO dbo.tblErrorLog
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_STATE() AS ErrorState,
ERROR_LINE() AS ErrorLine,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_MESSAGE() AS ErrorMessage,GETDATE(),
CONVERT(sysname ,USER_NAME())AS DBUSERNAME,
CONVERT(SYSNAME,SUSER_SNAME()) AS SYSUSERNAME
end catch
END
The code of uspUpdateAuditDetailFromAuditTableID needs a little bit of explanation. As you can see in the code, this stored procedure receives a parameter @pID which is the ID of tblAud. Another parameter is @pDBname which is the name of production database. Last parameter is @pTableName which is the name of the table from where it was initialized (production database table). Depending upon the parameters, uspUpdateAuditDetailFromAuditTableID parses the XML. The XML has <ChangedData > and <NewData> elements. The <ChangedData> element holds the information of the original data (before change). <NewData> holds the information of new data (after change). Both these data sets are inserted into ##ctbl and then, checked column by column to get the changed information. Then <ChangedData> element and <NewRec> element holds the complete record. This is also illustrated in the above XML example. This is done via trigger using inserted and deleted memory tables. Here we can decide which column(s) are changed and initialize our next table tblAudDetail with the changed data only as discussed in the XML example above. I am also inserting values of columns having ‘ID’ in their names. I am performing this to recognize the record based on the primary key. I am assuming here that the primary column name will have the ‘ID’ in its name.
2. Activate Service Broker on New Audit Database and your own database
Next step is to activate service broker on both databases.
ALTER DATABASE MyProductionDB
SET ENABLE_BROKER
WITH ROLLBACK IMMEDIATE
GO
ALTER DATABASE AuditDatabase
SET ENABLE_BROKER
WITH ROLLBACK IMMEDIATE
GO
Run the following script to create service broker objects in MyProductionDB.
use MyProductionDB
drop master key
CREATE MASTER KEY
ENCRYPTION BY PASSWORD = 'onteuhoeu'
GO
The master key is created. According to MSDN, “The database master key is a symmetric key used to protect the private keys of certificates and asymmetric keys that are present in the database.”
CREATE MESSAGE TYPE BLOB
VALIDATION = NONE
GO
A message type defines the name of a message and the validation that Service Broker performs on messages that have that name. Both sides of a conversation must define the same message types. BLOB is the name of the message type we are defining.
CREATE CONTRACT BLOB_Contract
(BLOB SENT BY ANY)
GO
A contract defines the message types that are used in a Service Broker conversation and also determines which side of the conversation can send messages of that type. Each conversation follows a contract. The initiating service specifies the contract for the conversation when the conversation starts. The target service specifies the contracts that the target service accepts conversations for.
CREATE QUEUE BLOB_Queue_Init
GO
Queues store messages. When a message arrives for a service, Service Broker puts the message on the queue associated with the service.
CREATE SERVICE BLOB_Service_Init
ON QUEUE BLOB_Queue_Init
(BLOB_Contract)
A Service Broker service is a name for a specific task or set of tasks. Service Broker uses the name of the service to route messages, deliver messages to the correct queue within a database, and enforce the contract for a conversation.
Run the following script to create service broker objects in AuditDatabase. Same types of objects as above are created on AuditDatabase. But with different names;
Use AuditDatabase
GO
CREATE MASTER KEY
ENCRYPTION BY PASSWORD = 'onteuhoeu'
GO
CREATE MESSAGE TYPE BLOB
VALIDATION = NONE
GO
create CONTRACT BLOB_Contract
(BLOB SENT BY ANY)
GO
CREATE QUEUE BLOB_AdtQueue_Remote1
GO
CREATE SERVICE BLOB_AdtSrv_Remote1
ON QUEUE BLOB_AdtQueue_Remote1
(BLOB_Contract)
GO
We need to alter the queue BLOB_AdtQueue_Remote1 to set its status to ON and associate procedure to be executed when a message is inserted in a queue.
ALTER QUEUE BLOB_AdtQueue_Remote1
WITH ACTIVATION
(
STATUS = ON,
PROCEDURE_NAME = dbo.uspInsertAuditRec ,
MAX_QUEUE_READERS = 5,
EXECUTE AS OWNER
)
GO
MAX_QUEUE_READERS specifies the maximum number of instances of the activation stored procedure that the queue starts simultaneously. The value of max_readers must be a number between 0 and 32767. I have set this value to 5 to avoid contention.
The security principal that owns the initiating service must have SEND permission on the target service. So we need to execute,
GRANT SEND ON SERVICE::BLOB_Service_Remote1 TO [Public];
GO
The SEND permissions are currently granted to the [Public]. Everyone can send message using this queue. I have done this for simplicity. You can specify a specific user to restrict the message sending via this service.
3. Create stored procedure to apply triggers on tables for auditing
** **
Next we will create stored procedures to apply triggers on the tables we need to Audit on the production database.
Use MyProductionDB
go
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go
-- =============================================
-- Author: Atif Sheikh
-- Create date: 28-May-2010
-- Description: Create Audit Trigger
-- =============================================
CREATE PROCEDURE [dbo].[uspCreateTriggerToAudit]
@TableName varchar(100)
AS
BEGIN
SET NOCOUNT ON;
Declare @SCHEMA varchar(100)
Declare @colnames varchar(max)
Declare @objid int
Declare @sql varchar(max)
Select @objid = object_id from sys.objects where name = @tableName
Select @TABLENAME = table_name, @Schema = Table_Schema from information_schema.tables where table_name = @tableName
Select @colnames = COALESCE(@colnames+', ','')+ '['+ sys.columns.name +']'
from sys.columns where object_id = @objid
and system_type_id not in (34,35,99,241,165,173)
--Select * from sys.types
SET @SQL='CREATE TRIGGER ['+@TABLENAME+'_AuditTrigger]
ON ['+@SCHEMA+'].['+@TABLENAME+']
AFTER UPDATE, INSERT, DELETE
AS
BEGIN
SET NOCOUNT ON;
DECLARE @auditBody varchar(max)
DECLARE @auditBody1 varchar(max)
DECLARE @AppUserID varchar(max)
DECLARE @Objectid bigint
DECLARE @DMLType CHAR(1)
Set @auditBody1 = ''''
Set @auditBody = ''''
Set @AppUserID = ''''
Select @Objectid = parent_object_id from sys.objects where name = ''' + @tableName + '_AuditTrigger''
if Exists(Select column_id from sys.columns where object_id = @Objectid and name = ''UpdatedBy'')
begin
--Select @AppUserID = UpdatedBy from inserted as ii
Set @AppUserID = ''''
end
IF NOT EXISTS (SELECT * FROM inserted)
BEGIN
SELECT @auditBody = (select '+ @colnames +' FROM deleted AS t FOR XML AUTO)
Select @DMLType = ''D''
END
-- after update or insert statement
ELSE
BEGIN
IF EXISTS (SELECT * FROM deleted)
begin
SELECT @auditBody = (select '+ @colnames +' FROM deleted AS t FOR XML AUTO)
SELECT @auditBody1 = (select '+ @colnames +' FROM Inserted AS t FOR XML AUTO)
SELECT @DMLType = ''U''
end
ELSE
begin
SELECT @auditBody = (select '+ @colnames +' FROM inserted AS t FOR XML AUTO)
SELECT @DMLType = ''I''
end
END
SELECT @auditBody =
''<AuditMsg>
<SourceDb>'' + DB_NAME() + ''</SourceDb>
<SourceTable>' + @tableName + '</SourceTable>
<UserId>'' + SUSER_SNAME() + ''</UserId>
<AppUserId>'' + @AppUserID + ''</AppUserId>
<DMLType>'' + @DMLType + ''</DMLType>
<ChangedData>'' + CAST(@auditBody AS NVARCHAR(MAX)) + ''</ChangedData>''
if @DMLType = ''U''
SELECT @auditBody = @auditBody + ''<NewRec>'' + @auditBody1 + ''</NewRec>''
SELECT @auditBody = @auditBody + ''</AuditMsg>''
if @auditBody <> ''''
begin
DECLARE @h UNIQUEIDENTIFIER
Declare @CXml varchar(max)
Set @CXml = @auditBody
BEGIN DIALOG CONVERSATION @h
FROM SERVICE BLOB_Service_Init
TO SERVICE ''BLOB_AdtSrv_Remote1''
ON CONTRACT BLOB_Contract
WITH ENCRYPTION=OFF ;
SEND ON CONVERSATION @h
MESSAGE TYPE BLOB (CONVERT(VARBINARY(max), @CXml))
end
END'
print @SQL
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
exec (@SQL)
END
The above stored procedure [dbo].[uspCreateTriggerToAudit] is created on the production database and it accepts parameter @TableName on which trigger is created on the fly. This stored procedure holds the generic dynamic sql. This sql will be used to create a trigger on the tables we need to audit. I will further explain later.
The following stored procedure on production database takes the comma separated list of object_id of tables we want to audit on Production database.
Use MyProductionDB
go
CREATE PROCEDURE [dbo].[uspGetAuditTables]
@pObjectID varchar(max) ,
@pAlias varchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
Declare @val varchar(1000)
Declare @val2 varchar(1000)
Declare @tab as Table (ObjectID bigint, Alias nvarchar(1000))
Insert into @tab
Select * from tblAuditTables
delete from tblAuditTables
insert into tblAuditTables (ObjectID,Alias)
select a.value,b.value from dbo.fnSplit(@pObjectID,',') a, dbo.fnSplit(@pAlias,',') b
where a.tid = b.tid
Declare C1 Cursor For select name from dbo.fnSplit(@pObjectID,',') inner join sys.objects on sys.objects.Object_id = value
Open C1
Fetch Next from C1 into @val
while @@Fetch_Status = 0
begin
print(@val)
if not Exists(Select object_id from sys.objects where name = rtrim(ltrim(@val)) + '_AuditTrigger' and Type = 'TR')
begin
Set @val = rtrim(ltrim(@val))
exec uspCreateTriggerToAudit @val
end
Fetch Next from C1 into @val
end
Close C1
Deallocate C1
Declare C1 Cursor For
Select sys.objects.name,sys.schemas.name from sys.objects inner join sys.schemas on sys.schemas.schema_id = sys.objects.schema_id where Object_id in
(Select objectid from @tab
Except
Select objectid from tblAuditTables)
Declare @sSql varchar(1000)
Open C1
Fetch Next from C1 into @val,@val2
while @@Fetch_Status = 0
begin
Set @sSql = 'Drop Trigger [' + @val2 + '].[' + @val + '_AuditTrigger]'
Exec (@sSql)
Fetch Next from C1 into @val,@val2
end
Close C1
Deallocate C1
END
The above stored procedure gets the names of the tables we need to audit. Here we execute the dynamic sql in uspCreateTriggerToAudit according to the object_ID passed to it. For example we have a table UserTable (object_id = 1011235) in the production database which we need to audit. To apply the trigger on this table, we will call the above mentioned stored procedure as;
exec [dbo].[uspGetAuditTables] ‘1011235’, ‘UserTable’
On executing you will see the trigger on the UserTable.
The following table tblAuditTables holds the list of tables that we are auditing. I did this so that we can have a quick check of the tables we are auditing.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[tblAuditTables](
[ObjectID] [int] NOT NULL,
[Alias] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_tblAuditTables] PRIMARY KEY CLUSTERED
(
[ObjectID] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
This table only keeps the record of the tables that are being audited.
Now change any value in UserTable. It will be reflected in tblAud as XML in xmlQuery column. You can create reports on Audit data from Audit database according to your requirements. I have created the Insert / Update /Delete statements of the production database from the AuditDatabase.
** **
Conclusion
After implementing the above solution, you will be able to track all the changes on the tables on which audit is applied. Also, you can filter out the tables of production database on which you don’t want to apply audit. Due to asynchronous messaging, performance will not be hurt due to the use of triggers. There is also no point of data loss as all the messages are maintained by SQL Server itself.
This long solution might have some drawbacks. I am currently using it on one of my systems in QA without any error (till now). And we are planning to ship it to production server. Any comments / suggestions are most welcome.