How to implement audit trail using triggers in SQL
How to implement audit trail using triggers in SQL
An audit trail is a method for tracking changes made by users in a database. In industries such as Healthcare, Finance, and Insurance, maintaining a detailed record of all modifications is essential to ensure accountability and prevent unintended alterations to the system.
An effective audit trail should provide a chronological log of changes, including:
- Who made the change
- What was changed
- When the change occurred
Additionally, systems can capture how the user made the change—whether through an application interface or an automated process.
Methods for Implementing an Audit Trail
- 1. ORM (Entity Framework) Change Tracking
- 2. SQL Server Audit Feature (Learn More)
- 3. Database Triggers
In this guide, we’ll demonstrate how to implement an audit trail in MS-SQL using triggers.
Step 1 - Creating a Table to Store Audit Trail Data
CREATE TABLE [dbo].[AuditTrail] ( [TableName] VARCHAR(100) NULL, [TableId] VARCHAR(100) NULL, [UpdatedBy] INT NULL, [UpdateDate] DATETIME NULL, [UpdateJSON] NVARCHAR(MAX) NULL );
This table is designed to store audit trail data for all tables in the database. The current approach stores change information in JSON format, though it can also be stored in XML or key-value columns based on your requirements. You can customize the script to align with your table design.
Column Descriptions:
- TableName – Specifies the table where the change occurred.
- TableId – Stores the primary key ID of the affected record (assuming a surrogate key).
- UpdatedBy – Records the user ID who made the update.
- UpdateDate – Captures the timestamp of the change.
- UpdateJSON – Stores the previous and new values in JSON format for tracking changes.
The goal of the audit trail process is to record every change made by users in this table, ensuring accountability and traceability.
Step 2 - Creating a Trigger in the Target Table
The following trigger is designed for a sample table called 'Customer'. You should update this script to match your specific table(s).
To understand this code, you need a basic knowledge of SQL Triggers. In summary, a trigger is invoked during bulk updates, requiring the use of standard collections like INSERTED and DELETED to capture the changed data.
The approach here is to generate a JSON string containing the modified fields. This can also be adapted to XML format if needed.
This trigger will insert a new record into the "AuditTrail" table for each modified record, with all changed field values stored in the UpdateJSON column.
ALTER TRIGGER [dbo].[tr_Customer] ON [dbo].[Customer] AFTER UPDATE AS BEGIN declare @json1 nvarchar(max) declare @json2 nvarchar(max) declare @updatedBy int declare @id int SELECT TOP 1 @id =CustomerId,@updatedBy = ISNULL(UpdatedBy, CreatedBy) FROM inserted ORDER BY InternalReqId WHILE @id IS NOT NULL BEGIN -- get json1 the old value SET @json1 = (select * from deleted WHERE CustomerId= @id FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) -- get json2 the new value SET @json2 = (select * from inserted WHERE CustomerId= @id FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) DECLARE @updateJson nvarchar(max) -- get the json for the updated columns SET @updateJson = (SELECT j1.[key], j1.[type], j1.[value] as [oldvalue], j2.[value] as [newvalue] from openjson(@json1) j1 left join openjson(@json2) j2 on j1.[Key] = j2.[key] where ISNULL(j1.[value], '') <> ISNULL(j2.[value], '') FOR JSON PATH, WITHOUT_ARRAY_WRAPPER ) IF (ISNULL(@updateJson, '') <> '') BEGIN -- store it in the output table INSERT INTO common.AuditTrail(TableName, TableId, UpdatedBy, UpdateDate, UpdateJSON) values('Customer', @id, @updatedBy, getdate(), @updateJson) END -- get next id SET @id = (SELECT TOP 1 CustomerId FROM inserted WHERE CustomerId> @id ORDER BY CustomerId) END END
Step 3 - Creating a View to Read Audit Trail Data
This view simplifies querying the AuditTrail data by listing each change, including the table name, field name, old value, and new value.
The code assumes the existence of an AppUser table, where the UpdatedBy user ID is stored. If your system uses a different method to retrieve user details, you should modify this accordingly.
Additionally, you may notice the array wrapper ([ & ]) used in the JSON structure. The trigger is designed to exclude this wrapper. If needed, you can adjust both the trigger and this view to match your specific requirements.
create or alter view common.vAuditTrail as select t.TableName, t.TableId, t.UpdatedBy, au.FirstName + ' ' + au.LastName UpdatedByName, t.UPdateDate, JSON_VALUE(j.value, '$.key') FieldName, JSON_VALUE(j.value, '$.type') FieldType, JSON_VALUE(j.value, '$.oldvalue') OldValue, JSON_VALUE(j.value, '$.newvalue') NewValue from dbo.AuditTrail t LEFT JOIN dbo.AppUser au ON t.UpdatedBy = au.AppUserId CROSS APPLY OPENJSON('['+t.UpdateJSON+']') j WHERE JSON_VALUE(j.value, '$.key') NOT IN ('UPdateDate', 'UpdatedBy')
Step 4 - Querying Table-Specific Audit Trail Data
The following is an example query to retrieve Audit Trail data for a specific table, such as 'Customer'.
If the Customer table contains foreign keys (e.g., StatusId referencing a Status table), you must retrieve both the old and new values of Status to display meaningful information in the Audit Report.
Additionally, your target table may contain multiple fields of different data types. You should ensure that each field's values are properly formatted and user-friendly in the final report.
SELECT t.*, CASE t.FieldName WHEN 'StatusId' THEN 'Status' END AS FieldNameDisplay, CASE t.FieldName WHEN 'StatusId' THEN lv1.ValueTitle END AS OldValueName, CASE t.FieldName WHEN 'StatusId' THEN lv2.ValueTitle END AS NewValueName from common.vAuditTrail t left join dbo.Status s1 ON t.FieldName = 'StatusId' AND try_convert(int, t.OldValue) = s1.StatusId left join dbo.Status s2 ON t.FieldName = 'StatusId' AND try_convert(int, t.NewValue) = s2.StatusId WHERE t.TableName = 'Customer'
Key Considerations:
- Ensure proper handling of data types for readability.
- Modify triggers and views based on business requirements.
- Adapt the JSON structure if necessary.
Join The Discussion