Introduction
In Today's IT world, data is everything, so providing data access to many users introduces a potential threat of unknown risks. We must take proper measures to capture data changes and updates to avoid such risks. Therefore, the data managers must apply tools to track each data activity done by the users who have read-write access to the data. This data tracking tool needs to be implemented at the field level for each available data element. In this article we will discuss such tools and cover the following topics:
- Setting up the database tables and triggers
- Generic and Custom triggers
- Implementing the triggers
- Testing and reading the data captured by triggers
- Few do's and don'ts to implementing Auditing properly.
Data Tracking Tools
There are several ways by which we can capture the data activity. For example:
- Capturing the data changes from the application code. This will not capture any changes made on the database directly.
- Capturing the data changes using SQL Triggers:
- Using a trigger capturing the entire row of data getting changed. This will capture redundant data which is not needed.
- Using a trigger capturing a single field/data element getting changed or updated
This article will discuss method 2.(2) of all the options mentioned above. The advantages of this method will be capturing any data changes made from either the application side or the database side. Moreover, this will save the changes for the specific data fields instead of the complete data set. Another advantage of using this trigger is during the testing of the application. We can use the trigger to ensure the system is not making any data submission errors or any other issues that go undetected by the testers during manual testing.
Data Tracking Trigger Types
To create data tracking triggers, you can use one of the below methods:
- A simple generic trigger with minimum maintenance – A simple trigger will capture all the data changes made by the user with exact database values. For example, if a user changes a City value from the Application Austin to Seattle. In the database the CityID gets changed from 10 to 20 (10 being the ID given to Austin and 20 being the ID given to Seattle). This simple trigger will capture the CityID’s instead of the City Names. Once we have implemented a simple trigger for a table, we do not have to make updates on the trigger even if there are some changes to the Table afterwards, hence no maintenance.
- Custom trigger that collects information specific to each field - A custom trigger provide better details to the business users. For example, if a user changes a City value from the Application, Austin to Seattle. In the database the CityID gets changed from 10 to 20 (10 being the ID given to Austin and 20 being the ID given to Seattle). This custom trigger will capture the City Name and not the ID’s. Once we have implemented a custom trigger for a table, we have to make updates on the trigger every time there are some changes to the Table afterwards, hence we need to maintain this trigger with changes to the table.
I recommend using simple generic triggers when the data tracking needs to be kept internal and does not need to be provided daily to the business users. On the other hand, use the custom triggers when the data tracking must be shared daily with business users. We will discuss both types of triggers in detail and examples in the following sections. Below diagram gives a high-level overview of data capturing.
Initial Setup
To get started, we need to set up the audit table for capturing all the data activities. This section will cover the scripts to set up a simple generic trigger and a custom trigger. Create both tables by using the provided sample scripts. This table will directly store all the data changes from the application or database. There is need of only one generic audit table and one custom audit table in the application database.
We assume that the reader of this article has basic knowledge and understanding of SQL, including triggers, and have access to SSMS to practice this concept.
Creating Generic Audit Table
CREATE TABLE [Audit].[GenericDataTracking]( [GenericDataTrackingID] [bigint] IDENTITY(1,1) NOT NULL Primary Key, [Type] [char](1) NOT NULL, [TableSchema] [nchar](5) NULL, [TableName] [nvarchar](128) NOT NULL, [PK] [varchar](128) NOT NULL, [FieldName] [nvarchar](128) NOT NULL, [OldValue] [nvarchar](max) NULL, [NewValue] [nvarchar](max) NULL, [UpdateDate] [datetime2](7) NOT NULL, [UserName] [nvarchar](128) NOT NULL, [Application] [nvarchar](150) NOT NULL )
Column details:
- GenericDataTrackingID - This is the primary key for the table.
- Type - This column will capture the changes if any are due to the Update or delete action.
- TableSchema - This column will contain the schema name for the updated table
- TableName - This column will contain the table name for the updated Table data
- PK - This column will contain the primary key of the Updated Row in the table. Note: All the tables we will be applying the triggers must always have a defined primary key.
- FieldName - This column will contain the field name(Column Name) for updated field.
- OldValue - This column will have the old value for the field ( the value before the change occurred).
- NewValue - This column will contain the New value for the field ( the new value which has been updated now).
- UpdatedDate - This column will contain the date and time when the change will occur.
- Username - This column will contain the username of the user who will make this change.
- Application - This column will contain the application name of the source of change. For example, SSMS or the Web Application
Creating Custom Audit Table
CREATE TABLE [Audit].[DataTrackingCustom]( [DataTrackingCustomID] [bigint] IDENTITY(1,1) NOT NULL Primary Key, [ClientID] [int] NOT NULL, [Type] [char](1) NULL, [TableSchema] [nchar](15) NULL, [TableName] [nvarchar](128) NULL, [PK] [varchar](128) NULL, [FieldName] [nvarchar](128) NULL, [OldValue] [nvarchar](max) NULL, [NewValue] [nvarchar](max) NULL, [UpdateDate] [datetime2](7) NULL, [UserName] [nvarchar](128) NULL, [Application] [nvarchar](150) NULL, [tabledisplayname] [nvarchar](128) NULL, [fielddisplayname] [nvarchar](128) NULL, [isviewable] [bit] NOT NULL )
The column details are defined here. These are used differently as compared to generic audit table.
- DataTrackingCustomID - This is the primary key for the table.
- ClientID - This column will contain the Client ID so you can easily display all the data changes for the client in one place.
- TableDisplayName - This column will contain a user-friendly name for the updated table/section.
- FieldDisplayName - This column will contain a user-friendly name of the updated Field/Column.
- IsViewable - As we capture all the data changes, some columns may not be advisable to display on the Web application. We control that data from the trigger and use this flag for the same purpose.
These two audit tables will store all the updated/changed data captured using the triggers. We will get into the details of triggers in next the sections.
Creating Database Tables
Now, let us understand the triggers with the help of an example. First, let's create two tables which we will use to change/update the data set captured by the triggers. I have created the Below mentioned tables. We will be using the [lu].[Gender] table for the generic trigger and [Client].[ClientProfile] table for the custom trigger.
CREATE TABLE [lu].[Gender]( [GenderId] [int] IDENTITY(1,1) NOT NULL Primary Key, --Primary key [Gendername] [nvarchar](250) NOT NULL, -- Gender names like - Male, Female [IsActive] [bit] NOT NULL, -- Capture if the value is active or inactive [createdby] [nvarchar](36) NULL, -- user who created this record [datecreated] [datetime2](7) NOT NULL, -- date and time when the record was created [updatedby] [nvarchar](36) NULL, -- user who last modified this record [dateupdated] [datetime2](7) NULL -- date and time when this record was last modified ) CREATE TABLE [Client].[ClientProfile]( [ClientProfileId] [int] IDENTITY(1,1) NOT NULL Primary Key, [FirstName] [nvarchar](250) NULL, [LastName] [nvarchar](250) NULL, [GenderId] [int] NOT NULL, --FK from Lu.Gender Table [dob] [datetime2](7) NULL, [CreatedBy] [nvarchar](50) NOT NULL, [DateCreated] [datetime2](7) NOT NULL, [ModifiedBy] [nvarchar](50) NULL, [DateModified] [datetime2](7) NULL ) -- adding gender as foreign key reference to clientprofile table ALTER TABLE [client].[ClientProfile] WITH CHECK ADD CONSTRAINT [FK_ClientProfile_Gender_GenderID] FOREIGN KEY([GenderId]) REFERENCES [lu].[Gender]([GenderId]) GO
Please note: for executing the trigger, the tables should always have this column modifiedby and for this system to work there must be a primary key in each table.
Simple Generic Trigger
Below is the script for the simple generic trigger. First, create this trigger on the [lu].[Gender] table we have discussed in the previous section. Once this trigger is added to the [lu].[Gender] table, we will run some test scenarios to see the results.
Note: To run this trigger, the following things need to be done:
- Populate the Updatedby column correctly with the Logged in User's Username whenever changes are made.
- When deleting the entire row from the table, the code needs to populate the updatedby column first then make the hard delete.
Create TRIGGER [lu].[tr_gender] ON [lu].[Gender] FOR INSERT, UPDATE, DELETE AS DECLARE @bit INT , @field INT , @fieldId INT , @maxfield INT , @char INT , @fieldname VARCHAR(128) , @TableName VARCHAR(128) , @SchemaName VARCHAR(128) , @PKCols VARCHAR(1000) , @sql VARCHAR(2000), @UpdateDate VARCHAR(21) , @UserName VARCHAR(128) , @Type CHAR(1) , @FullTableName VARCHAR(256), @PKSelect VARCHAR(1000), @next bit, @oldvalue nVARCHAR(1000), @newvalue nVARCHAR(1000) SELECT @TableName = 'Gender' , @schemaName = 'lu' ---------------------------------------------------------------------------------------------- SELECT @FullTableName ='[' + @SchemaName + '].[' + @TableName + ']' -- selecting the action Insert, delete or update SET @Type = (CASE WHEN EXISTS(SELECT * FROM INSERTED) AND EXISTS(SELECT * FROM DELETED) THEN 'U' -- Set Action to Updated. WHEN EXISTS(SELECT * FROM INSERTED) THEN 'I' -- Set Action to Insert. WHEN EXISTS(SELECT * FROM DELETED) THEN 'D' -- Set Action to Deleted. ELSE NULL -- Skip. It may have been a "failed delete". END) -- date for the action SELECT @UpdateDate = CONVERT(VARCHAR(8), GETDATE(), 112) + ' ' + CONVERT(VARCHAR(12), GETDATE(), 114) -- username if changes are done from SSMS SELECT @UserName =Right(SYSTEM_USER,LEN(SYSTEM_USER)-9) --You may need to adjus this depending on the username structure for your SSMS -- capture username if the action is done from the application -- do not use this for provider table, state table if APP_NAME() = 'Core Microsoft SqlClient Data Provider' --Application Frameworkname, change as per your application framework name BEGIN if @type in ('U') Begin SELECT top(1) @UserName = i.modifiedby from inserted i End if @type in ('D') Begin SELECT top(1) @UserName = d.modifiedby from deleted d End IF @type IN ('I') -- We are not capturing inserts, so you may remove this step BEGIN SELECT TOP(1) @UserName = i.createdby FROM inserted i END END -- 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 ANDc.CONSTRAINT_SCHEMA = pk.TABLE_SCHEMA -- Get primary key select for insert SELECT @PKSelect = COALESCE(@PKSelect+'+','') + '''' + '''+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 -- error handling IF @PKCols IS NULL BEGIN RAISERROR('no PK on table %s', 16, -1, @TableName) RETURN END -- initiating variables SELECT @field = 0, @maxfield = MAX(ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = @SchemaName --- main while loop------------- WHILE @field < @maxfield +1 BEGIN -- fetching the field names for each column SELECT @fieldname = COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName AND ORDINAL_POSITION = @field AND TABLE_SCHEMA = @SchemaName -- fetching the column ID, these can be different from the ordinal position if the columns are dropped SELECT @fieldid = COLUMNPROPERTY(OBJECT_ID(@FullTableName), @fieldname, 'ColumnID') -- fetching the ordinal position SELECT @field = MIN(ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = @SchemaName AND ORDINAL_POSITION > @field -- capturing some values to do the condition for the updated action SELECT @bit = (@fieldId - 1 )% 8 + 1 SELECT @bit = POWER(2,@bit - 1) SELECT @char = ((@fieldId - 1) / 8) + 1 Select @next = 1 -- ignoring these fields as audit captures data about who and when the data was changed, also the inserts ignored from the audit table as they are in the actual tables itself. And we are skipping the inserts also. Our Data tracking trigger is focussed on Updates and deleted if @fieldname IN ('datemodified','datecreated','createdby','modifiedby') or @type = 'I' Begin Select @next = 0 end ------------------------------------------------------------------------------------------------------------- IF @next = 1 BEGIN SELECT @oldvalue ='d.'+@fieldname SELECT @newvalue = 'i.'+@fieldname --------------------------- Skip apostrophe in the updated values IF CHARINDEX('''', @newvalue)>0 SET @newvalue=REPLACE(@newvalue,'''','''''') IF CHARINDEX('''', @oldvalue)>0 SET @oldvalue=REPLACE(@oldvalue,'''','''''') ---------------------------------------------------------------------------- -- the frist condition checks for updates and next one for deletes IF SUBSTRING(COLUMNS_UPDATED(),@char, 1) & @bit > 0 OR @Type IN ('D') BEGIN SELECT @sql = ' insert [Audit].[GenericDataTracking] ( Type, TableSchema, TableName, PK, FieldName, OldValue, NewValue, UpdateDate, UserName, Application) select ''' + @Type + ''',''' + @SchemaName +''', '''+ @TableName + ''',' + @PKSelect + ', ''' + @fieldname + '''' + ', '+@oldvalue + ', '+@newvalue+ ',''' + @UpdateDate + '''' + ', ''' + @UserName + ''''+ ', ''' + REPLACE(APP_NAME(), '''', '''''') + '''' + ' 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) -- excuting the sql into the GenericDataTracking table END END END -- while loop end
Testing the Generic Trigger
Now we have everything setup for the generic trigger. Let's test with a simple update command on the [lu].[Gender]. Follow the below steps for testing:
- As we are testing the generic trigger in [lu].[Gender] table, below image shows the data available in this table.
- The audit tracking table will appear to be empty currently. Below is the image for the generic audit table.
- Now we can run some tests to update some data fields in the [lu].[Gender] table. I am using the below scripts for running the test. I am updating the gender value "Male" to "Male-ABC".
- After running the above mentioned script, we will see the data being captured in the audit table. Below is the image from the audit table showing the captured updates.
- In the audit table, we get the table name, schema name, type of update, primary key for the row that was updated, old value, new value, date/time of the update and the user who updated the field and the source of update.
Custom Trigger
Below is the script for the custom trigger. First, create this trigger on the [Client].[ClientProfile] table we have discussed in the previous section. Once this trigger is added to the [Client].[ClientProfile] table we will run some test scenarios to see the results.
Note: To run this trigger, the following things need to be done:
- Populate the Modifiedby column correctly with the Logged in User's Username whenever changes are made.
- When deleting the entire row from the table, the code needs to populate the Modifiedby column first then make the hard delete.
The purpose of custom trigger is specifically to provide understandable data tracking updates to the business users. This trigger provides proper naming for the field names and proper data names instead of any ID values. Run the below script to add the custom trigger to the [Client].[ClientProfile] table.
USE [TAP] GO /****** Object: Trigger [client].[tr_ClientProfile] Script Date: 11/02/22 11:19:11 PM ******/SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO ALTER TRIGGER [client].[tr_ClientProfile] ON [client].[ClientProfile] FOR INSERT, UPDATE, DELETE AS SET NOCOUNT ON DECLARE @bit INT ,@field INT ,@fieldId INT ,@maxfield INT ,@char INT ,@fieldname nVARCHAR(128) ,@fieldname1 nVARCHAR(128) , @FieldDisplayName nVARCHAR(128),@TableDisplayName nVARCHAR(128),@SchemaName VARCHAR(128) ,@TableName nVARCHAR(128) ,@PKCols VARCHAR(1000) , @sql VARCHAR(max), @sql1 VARCHAR(max),@UpdateDate VARCHAR(21) ,@UserName VARCHAR(128) ,@Type CHAR(1) ,@PKSelect VARCHAR(1000) , @ClientID VARCHAR(1000) ,@FullTableName VARCHAR(256),@oldvalue nVARCHAR(1000),@newvalue nVARCHAR(1000),@oldvalue1 nVARCHAR(1000), @parentID char(10), @newvalue1 nVARCHAR(1000),@isviewable char(1),@next bit,@lastfield VARCHAR(1000),@lastusername VARCHAR(1000),@inserts bit, @name char(3), @followup char(1) SELECT @TableName = 'ClientProfile' -- selecting schema SELECT @schemaName = 'client' ------------------------------------------------------------ --putting the whole trigger in this condition to run only once and not in a nested trigger IF TRIGGER_NESTLEVEL() <= 1 Begin SELECT @FullTableName ='['+ @SchemaName + '].[' + @TableName + ']' SET @Type = (CASE WHEN EXISTS(SELECT * FROM INSERTED) AND EXISTS(SELECT * FROM DELETED) THEN 'U' -- Set Action to Updated. WHEN EXISTS(SELECT * FROM INSERTED) THEN 'I' -- Set Action to Insert. WHEN EXISTS(SELECT * FROM DELETED) THEN 'D' -- Set Action to Deleted. ELSE NULL -- Skip. It may have been a "failed delete". END) -- date for the action SELECT @UpdateDate = CONVERT(VARCHAR(8), GETDATE(), 112) + ' ' + CONVERT(VARCHAR(12), GETDATE(), 114) -- username if changes are done from SSMS SELECT @UserName =Right(SYSTEM_USER,LEN(SYSTEM_USER)-9) -- capture username if the action is done from the application if APP_NAME() = 'Core Microsoft SqlClient Data Provider' BEGIN if @type in ('U') Begin SELECT top(1) @UserName = i.[ModifiedBy] from inserted i End if @type in ('D') Begin SELECT top(1) @UserName = d.ModifiedBy from deleted d End END -- 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 pk.TABLE_SCHEMA = @SchemaName AND CONSTRAINT_TYPE = 'PRIMARY KEY' AND c.TABLE_NAME = pk.TABLE_NAME AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME AND c.CONSTRAINT_SCHEMA = pk.TABLE_SCHEMA -- Get primary key select for insert SELECT @PKSelect = COALESCE(@PKSelect+'+','') + '''' + '''+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 pk.TABLE_SCHEMA = @SchemaName AND CONSTRAINT_TYPE = 'PRIMARY KEY' AND c.TABLE_NAME = pk.TABLE_NAME AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME AND c.CONSTRAINT_SCHEMA = pk.TABLE_SCHEMA --------Specific to Custom Trigger-- -- Get ClientID for insert for each update or deleted SELECT @ClientID = COALESCE(@ClientID+'+','') + '''' + '''+convert(varchar(100),coalesce(i.' + COLUMN_NAME +',d.' + COLUMN_NAME + '))+''''' FROMINFORMATION_SCHEMA.KEY_COLUMN_USAGE c WHEREc.TABLE_NAME = @TableName ANDc.TABLE_SCHEMA = @SchemaName ANDc.COLUMN_NAME = 'ClientProfileId' --------Specific to Custom Trigger-- -- error handling IF @PKCols IS NULL BEGIN RAISERROR('no PK on table %s', 16, -1, @FullTableName) RETURN END --------Specific to Custom Trigger-- --Display values for table SET @TableDisplayName = 'Client Profile' --------Specific to Custom Trigger-- -- initiating variables SELECT @field = 0, @maxfield = MAX(ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = @SchemaName --- main while loop------------- WHILE @field < @maxfield + 1 BEGIN -- fetching the field names for each column SELECT @fieldname = COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = @SchemaName AND ORDINAL_POSITION = @field -- fetching the column ID, these can be different from the ordinal position if the columns are dropped SELECT @fieldid = COLUMNPROPERTY(OBJECT_ID(@FullTableName), @fieldname, 'ColumnID') -- inserting initial NULLS in old and new value variables select @oldvalue = NULL Select @newvalue = NULL -- fetching the ordinal position SELECT @field = MIN(ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = @SchemaName AND ORDINAL_POSITION > @field -- capturing some values to do the condition for the updated action SELECT @bit = (@fieldId - 1 )% 8 + 1 SELECT @bit = POWER(2,@bit - 1) SELECT @char = ((@fieldId - 1) / 8) + 1 --------Specific to Custom Trigger-- --display values for fields SET @FieldDisplayName = CASE @fieldname WHEN 'createdby' THEN 'Record Created By'WHEN 'datecreated' THEN 'Record Creation Date' WHEN 'Modifiedby' THEN 'Record Last Modified By' WHEN 'datemodified' THEN 'Record Last Modified Date' WHEN 'ClientProfileID' THEN 'Client Profile ID' WHEN 'FirstName' THEN 'First Name'WHEN 'LastName' THEN 'Last Name' WHEN 'GenderID' THEN 'Client Gender' When 'dob' THEN 'Date of Birth'END -- Managing Is viewable field for all types-------------------------- select @isviewable = '1' select @inserts = 0 if @type in('U') begin select @isviewable = '1' end --------Specific to Custom Trigger-- -- ignoring inserts and the created by and modified by fields if @type in('I') or @fieldname in ('createdby','datecreated','modifiedby','datemodified') begin select @inserts = 1 end -----------setting next at default 0 Select @next = 0 --------Specific to Custom Trigger-- ---field specific values if @fieldname = 'GenderID' BEGIN select @oldvalue =c.Gendername from lu.Gender c inner join deleted d on d.GenderID = c.GenderID select @newvalue = c.Gendername from lu.Gender c inner join inserted i on i.GenderID = c.GenderID Select @next = 1 end if @fieldname = 'dob' BEGIN select @oldvalue = d.dob from deleted d select @oldvalue = isnull(@oldvalue,'NULL') if @oldvalue <> 'NULL' set @oldvalue = convert(varchar,convert(datetime,left(@oldvalue,10)),106) select @newvalue = i.dob from inserted i select @newvalue = isnull(@newvalue,'NULL') if @newvalue <> 'NULL' set @newvalue = convert(varchar,convert(datetime,left(@newvalue,10)),106) Select @next = 1 end --------Specific to Custom Trigger-- --------------------------- Skip apostrophe in the updated values if CHARINDEX('''', @newvalue)>0 set @newvalue=Replace(@newvalue,'''','''''') if CHARINDEX('''', @oldvalue)>0 set @oldvalue=Replace(@oldvalue,'''','''''') ---------------------------------------------------------------------------- --------------------------- field specific value in insert ---------------------------------------------------------------------------- If @next = 1 Begin Select @oldvalue = ''''+ @oldvalue+'''' Select @newvalue = ''''+ @newvalue+'''' end ---------------------------------------------------------------------------- --------------------non field specific ---------------------------------------------------------------------------- if @next = 0 begin select @oldvalue ='d.'+@fieldname select @newvalue = 'i.'+@fieldname End --------------------adjusting NULLS for field specific values-------------------------------------------------------- if @type = 'D' or @newvalue in (NULL, 'NULL','') begin select @newvalue = '''--''' end If @oldvalue in (NULL, 'NULL', '') Begin select @oldvalue = '''--''' end If (@newvalue = '--' and @oldvalue = '--') or(@oldvalue = ' ' and @newvalue = 'NULL') OR (@oldvalue = '' AND @newvalue IS NULL) Begin Set @isviewable = 0 END --if @inserts = 0 --BEGIN if (substring(COLUMNS_UPDATED(),@char, 1) & @bit > 0 or @Type in ('I','D') ) AND @inserts = 0 BEGIN SELECT @sql = ' insert [Audit].[DataTrackingCustom] ( Type,TableSchema,TableName,tabledisplayname,fielddisplayname, PK, ClientID,IsViewable,FieldName, OldValue, NewValue, UpdateDate, UserName,Application) select ''' + @Type + ''','''+ @SchemaName + ''',''' + @TableName + ''',''' + @TableDisplayName + ''' ,''' + @FieldDisplayName + ''',' + @PKSelect + ',' + @ClientID+ ', ' + @isviewable+' ,''' + @fieldname + ''''+ ', '+@oldvalue+','+@newvalue+',' + '''' + @UpdateDate + ''''+ ' ,''' + @UserName + ''''+ ',''' + REPLACE(APP_NAME(), '''', '''''') + '''' + ' 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 -----------------While End END-------------nested trigger end
Testing the Custom Trigger
Now we have everything setup for the custom trigger. Let's test with a simple update command on the [client].[ClientProfile] table. Follow the below steps for testing:
- As we are testing the custom trigger in [client].[ClientProfile] table, below image shows the data available in this table.
- The audit tracking table will appear to be empty currently. Below is the image for the custom audit table.
- Now we can run some tests to update some data fields in the [client].[ClientProfile] table.
- Test 1 - updating "dob" field value for a client. I am using the below script for running the test 1. I am updating the dob value "01-01-1980" to "01-01-1988".
- Test 2 - updating "gender field value for a client. I am using the below script for running test 2. I am updating the genderID value from 1 (Male) to 2(Female).
- After running the above mentioned script, I will see the data being capture in the custom audit table. Below is the image from the audit table showing the captured updates.
- In the above image we can see the data updates captured for the two test cases performed.
Conclusion
Triggers are a convenient and easy way to keep data tracking for your databases independent of how they are accessed. There is minor maintenance work involved with Custom triggers, whereas generic triggers are primarily maintenance-free. In this article we covered creation of Audit table, creating some test table and some Trigger execution script providing us with the results we needed. These triggers can be replicated on all the database tables where data auditing is required. This will help you to setup your data tracking system in place.