16

I've been asked to create a simple DataGrid-style application to edit a single table of a database, and that's easy enough. But part of the request is to create an audit trail of changes made, who made them, and the date/time.

How might you solve this kind of thing?

(I'll be using C# in VS2008, ADO.NET connected to SQL Server 2005, WPF and Xceed's DataGrid, if it makes any difference.)

ewall
  • 27,179
  • 15
  • 70
  • 84
  • 2
    Do you want to audit the changes to the content or the table structure itself ? I suspect you mean the content and not the schema, but the phrase "to edit a single table of a database" is a bit dubious. Schema changes can also be audited by triggers FYI ;) – sindre j Nov 25 '09 at 16:56
  • What is the granularity of required audit? You can do it at tuple (row) level (i.e. some attribute is changed, but users are not interested in details) or at row AND attribute (column) level (i.e. users are interested in each attribute change). They imply a different schema design. – MaD70 Nov 25 '09 at 17:36
  • Besides, even if you are assured that your program is the only one accessing that table, assume otherwise or future requirements changes will catch you. – MaD70 Nov 25 '09 at 17:42
  • 1
    Ah, yes... good questions all. For my purposes, I only need to keep a history (configuration value changed, date/time, and username) of the changes to a few specific tables in a low-traffic database. It would clearly be more complicated if updates were more frequent or it was required to audit schema changes and such. – ewall Nov 25 '09 at 21:55

8 Answers8

12

There are two common ways of creating audit trails.

  1. Code your data access layer.
  2. In the database itself using triggers.

There are advantages and disadvantages to both. Some people prefer one over the other. It's often down to the type of app and the type of database use you can expect.

If you do it in your DA layer it's pretty much up to you. You just need to add code to every method that saves to the database to also save a log of the changes. This auditing code could be in your DA layer code, or even in your stored procs in your database if you are using stored procs for everything. Essentially the premise is the same, any time you make a change to the database, log that change.

If you want to go down the triggers route, you can write custom triggers for each table, or fashion a more generic trigger that works the same on lots of tables. Check out this article on audit triggers. This works by firing of triggers whenever a change is made, and the triggers log the changes. Remember that if you want to audit SELECT statements, you can't use triggers, you'll have to do that with in code/stored proc auditing. It's also worth remember that depending on your database, triggers may not fire in all circumstances. For example, most databases don't fire triggers during TRUNCATE statements. Check that your triggers get fired in any case that you need auditing.

Alternately, you could also take a look at using the service broker to do async auditing on a dedicated machine. This is more complex and takes a bit of configuring to set up.

Which ever way you do it you need to decide on the format the audit log will take. Normally you would save this log in your database, but you could just save it in a log file or whatever suits your requirements. You could use a single audit table that logs all changes, or you could have an audit table per main table being audited. For large scale implementations you could even consider putting the audit tables in a totally separate database. If your logging into a table, it's common to have a "change type" field which indicates if the audited change was an insert, update or delete style of change, along with the changed data, user who made the change and the date/time the change was made. Don't forget to include the old and new data for update style changes.

Simon P Stevens
  • 27,303
  • 5
  • 81
  • 107
  • 2
    Audit code in application programs/store procedures is a bad idea: a different program/interactive user that access that table can potentially bypass auditing. – MaD70 Nov 25 '09 at 17:20
  • Excellent--that's the direction I needed. The article on audit triggers is quite well-written, too (although I had to dig a little to get the link to work: http://weblogs.asp.net/jgalloway/archive/2008/01/27/adding-simple-trigger-based-auditing-to-your-sql-server-database.aspx?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+jongalloway+%28Jon+Galloway%29 ). – ewall Nov 25 '09 at 17:47
  • 2
    NEver do aufditing inteh data acces layer, that is just asking for trouble. There are other ways to change the data that need to be recorded. This is something that MUST be done in the database if you wanta full audit trail. – HLGEM Nov 25 '09 at 18:06
  • mmm.. the design detailed in that article is an Entity-Attribute-Value model (EAV) (http://en.wikipedia.org/wiki/Entity-attribute-value_model). Suggested reading: http://tonyandrews.blogspot.com/2004/10/otlt-and-eav-two-big-design-mistakes.html and http://www.simple-talk.com/opinion/opinion-pieces/bad-carma/ – MaD70 Nov 25 '09 at 18:25
  • 1
    I think what's important is to apply these techniques with consideration. Neither are particularly suitable to very large scale high load databases. Auditing in data access code can work in some circumstances, but you have to consider the cons, which as MaD70 says is that it's possible to bypass the auditing. Like wise, with a generic style trigger, you can have problems processing the data if the scheme changes as the audit trail may longer map to the scheme. There is no perfect way, but you just need to carefully consider each, there are several good answers here. – Simon P Stevens Nov 25 '09 at 19:36
  • Thanks for the links, MaD70 -- good reading! As I mentioned in an earlier comment, my needs here are very simple--I'm just keeping a record of a few fields that only change a few times a year. I don't expect ever to need the audit table for anything more than a more-or-less human-readable list. I tried another trigger which generated a separate audit table for each table I wanted the history for, and included both the old and the new values, but it was more than necessary... so I went back to the single-table EAV-ish design. – ewall Nov 25 '09 at 22:03
  • Oh, and this is kind of amusing -- the database (and related application) in question, which was designed by someone else a couple years ago, makes heavy use of a very poorly designed "One True Lookup Table". Ugh! – ewall Nov 25 '09 at 22:13
  • Article moved: http://weblogs.asp.net/jongalloway/adding-simple-trigger-based-auditing-to-your-sql-server-database Still useful today? –  Jan 15 '16 at 07:25
9

Ditto use triggers.

Anyone considering soft deletion should have a read of Richard Dingwall's The trouble with soft delete.

martin clayton
  • 76,436
  • 32
  • 213
  • 198
  • 1
    Interestingly, the mentioned article says "Audit logging at the row level sucks. Do it higher up where you know the full story." So triggers may suffice for this particular question, but are not _the_ solution for audit trails in general. Good read! – Niels van der Rest Feb 14 '14 at 13:57
  • Thanks for sharing this article. Made me realize that one of our application's usage of soft delete _still requires a developer_ to create a script to perform undeletes. No tangible benefit to the end user. – chrnola Apr 17 '14 at 15:07
8

Most universal method would be to create another table for storing versions of record from the first table. Then, you can remove all the data from main table. Suppose you need versioning of a table Person(PersonId, Name, Surname):

CREATE TABLE Person 
(
   PersonId INT,                   // PK
   CurrentPersonVersion INT        // FK
);

CREATE TABLE PersonVersion
(
  PersonVersionId INT,             // PK
  PersonID                         // FK 
  Name VARCHAR,                    // actual data
  Surname VARCHAR,                 // actual data

  ChangeDate                       // logging data
  ChangeAuthor                     // logging data
)

Now any change requires inserting new PersonVersion and updating the CurrentPersonVersionID.

PanJanek
  • 6,593
  • 2
  • 34
  • 41
2

The best way to do this is set up triggers in the database that write to audit tables.

HLGEM
  • 94,695
  • 15
  • 113
  • 186
  • Depending on how you want the auditing to be used, all tables being audited could write to a single table. This allows a tick in the box for 'yes, everything is audited', without overcomplicating things. If you actually need to read it, then not so good... – Paddy Nov 25 '09 at 16:38
  • 2
    ONe audit table ,very bad idea. You will get locking issues on inserts with many users as everything will go to that table. – HLGEM Nov 25 '09 at 18:04
2

Solution 1: SQL Server Change Data Capture

https://learn.microsoft.com/en-us/sql/relational-databases/track-changes/enable-and-disable-change-data-capture-sql-server?view=sql-server-2017

First you need to enable change data capture on your database

USE AdventureWorks2012
GO  
EXEC sys.sp_cdc_enable_db  
GO  

Then you can query the changes using fn_cdc_get_all_changes_ or fn_cdc_get_net_changes_.

-- ========  
-- Enumerate All Changes for Valid Range Template
-- ========  
USE AdventureWorks2012;  
GO  

DECLARE @from_lsn binary(10), @to_lsn binary(10);  
SET @from_lsn = sys.fn_cdc_get_min_lsn('HR_Department');  
SET @to_lsn   = sys.fn_cdc_get_max_lsn();  

SELECT * FROM cdc.fn_cdc_get_all_changes_HR_Department  
(@from_lsn, @to_lsn, N'all');  

Solution 2: SQL Server Database Auditing

Source : https://www.dbaservices.com.au/how-to-configure-sql-server-auditing/

ENABLE DATABASE AUDITING

Database auditing requires that a server audit (although not necessarily server audit specification) to be in place. The DB auditing however is created within the user database that is to be audited, rather than within the master database where the server audit gets created. Database audit specifications can be found within the DB itself under Security –> Database Audit Specifications.

To create a database audit, you’ll need to first USE the database (to select it), then the following provides an example syntax for auditing SELECT, UPDATE and DELETE operations for specific tables within that database;

USE UserDatabase
GO


CREATE DATABASE AUDIT SPECIFICATION [User_Database_Audit_Specification]  
FOR SERVER AUDIT [SQL_Server_Audit]
     ADD (SELECT , UPDATE , DELETE ON UserDatabase.dbo.Customer_DeliveryAddress BY dbo )
    ,ADD (SELECT , UPDATE , DELETE ON UserDatabase.dbo.DimCustomer_Email BY dbo )
    ,ADD (SELECT , UPDATE , DELETE ON UserDatabase.dbo.DimCustomer_Phone BY dbo )
WITH (STATE = ON) ;   
GO

The SELECT, UPDATE and DELETE operations aren’t the only things you can add to the audit specification though…

+------------+-------------------------------------------------------------------+
| Action     | Description                                                       |
+------------+-------------------------------------------------------------------+
| SELECT     | This event is raised whenever a SELECT is issued.                 |
| UPDATE     | This event is raised whenever an UPDATE is issued.                | 
| INSERT     | This event is raised whenever an INSERT is issued.                | 
| DELETE     | This event is raised whenever a DELETE is issued.                 | 
| EXECUTE    | This event is raised whenever an EXECUTE is issued.               | 
| RECEIVE    | This event is raised whenever a RECEIVE is issued.                | 
| REFERENCES | This event is raised whenever a REFERENCES permission is checked. | 
+------------+-------------------------------------------------------------------+

The full list of database events you can log is available here:

https://learn.microsoft.com/en-us/sql/relational-databases/event-classes/security-audit-event-category-sql-server-profiler?view=sql-server-2017

Justin Lessard
  • 10,804
  • 5
  • 49
  • 61
1

I was recently faced with a requirement to audit some tables and I opted to use triggers. Like others, I only wanted to see entries in the audit table for those fields that had actually changed, however, when updating the tables, the application was updating all the fields in row whether they'd changed or not, therefore, checking whether the fields had been updated or not availed me nothing - they all had!

What I wanted, therefore, was a method of checking the actual value in each field to see if it had changed or not and only writing it to the audit table if it had. Having been unable to find any solution to this conundrum anywhere, I came up with my own, as follows:

CREATE TRIGGER [dbo].[MyTable_CREATE_AUDIT]
ON [dbo].[MyTable]
AFTER UPDATE

AS

INSERT INTO MyTable_Audit 
(ItemID,LastModifiedBy,LastModifiedDate,field1,field2,field3,
field4,field5,AuditDate)
SELECT i.ItemID,i.LastModifiedBy,i.LastModifiedDate,

field1 = 
  CASE i.field1
    WHEN d.field1 THEN NULL
    ELSE i.field1
  END,

field2 = 
  CASE i.field2
    WHEN d.field2 THEN NULL
    ELSE i.field2
  END,

field3 = 
  CASE i.field3
    WHEN d.field3 THEN NULL
    ELSE i.field3
  END,

field4 = 
  CASE i.field4
    WHEN d.field4 THEN NULL
    ELSE i.field4
  END,  

field5 = 
  CASE i.field5
    WHEN d.field5 THEN NULL
    ELSE i.field5
  END,

GETDATE()

FROM inserted i
INNER JOIN deleted d
ON i.ItemID = d.ItemID

As you can see, I'm comparing the values of each field in the deleted and inserted tables and only writing the field value from the inserted table to the audit table if they differ, otherwise I just write NULL.

It certainly works for me. Can anyone see any issues with this approach? My team own both the application and the database so possible curved balls like schema changes are covered off.

Jon White
  • 11
  • 1
  • NB: Writing both the i.field1 and d.field1 will save you having to look to the previous row to find out what the values were last time. Also, I'm not sure how this method allows us to determine whether a null is written because no change occurred, or because the value was set to null. If your log table has 2 rows, and in row 1, field1 is 'a' and in row 2 field1 is NULL, is that because field1 is still 'a' or because it was updated to null.. Things to think about for others adopting this approach – Caius Jard Oct 10 '17 at 11:17
1

The other way of doing this apart from triggers is this,

  1. Have four columns, UpdFlag, DelFlag, EffectiveDate and TerminatedDate for each table you want to do an audit trail on.
  2. code your sproc's in such a way that when you do an update, to pass in the all of the row's column data into the sproc, update the row by setting the TerminatedDate to the date that was updated, and mark the UpdFlag and to put in the datetime into the column
  3. Then create a new row with the new data (which is really updated). and put in a new date now for the EffectiveDate and the TerminatedDate set to the max date.

Likewise if you want to do a deletion of the row, simply update the row by marking the DelFlag as set, the TerminatedDate with the datetime now. You are in effect doing a soft delete and not an actual sql's Delete.

In that way, when you want to audit the data, and to show a trail of the changes, you can simply filter the rows for those that have the UpdFlag set, or between EffectiveDate and TerminatedDate. Likewise for those that were deleted, you filter for those that have the DelFlag set or between EffectiveDate and TerminatedDate. For the current rows, filter the rows that have both flags set off. The advantage is you don't have to create another table for the audit when the trigger is used!

Nerdroid
  • 13,398
  • 5
  • 58
  • 69
t0mm13b
  • 34,087
  • 8
  • 78
  • 110
  • I repeat myself: what if a different program/an interactive user bypass such store procedures? With triggers and appropriate access control you can be (quite, depending on DBMS) sure that each change is audited. – MaD70 Nov 25 '09 at 17:34
  • @MaD70: That's true, but the security is enforced by going down the proper route and enforces the usage of such application to do so. Bypassing sprocs which imho that would be classified as a security breach - like why should it be bypassed which defeats the point of an audit? Thanks for your input and it has given me food for thought! ;) Take care! – t0mm13b Nov 25 '09 at 17:51
  • 1
    Tommie, if I'm doing an update of all the records for a particular client I'm not going to use some stored proc the application uses and run it ina loop 100,000 times. Nor will I use it for data imports nor will people trying to commit fraud or just trying to destroy data becasue they are mad. – HLGEM Nov 25 '09 at 18:08
  • 1
    @tommieb75: you are welcome. In real life the risk that no one will remember (or is left) to enforce such **implicit** policy is high, so better to put it in the DB and let the DBMS enforce it (appropriate accompanying design documentation is also recommended). – MaD70 Nov 25 '09 at 18:13
0

I'd go triggers route, by creating table with similar structure to updated one, with additional columns for tracking changes like ModifiedAt etc. And then adding on update trigger that will insert changes to that table. I find it easier to maintain than have everything in the application code. Ofcourse many people tend to forget about triggers when it comes to questions like 'wtf this table is changing' ;) Cheers.

pablox
  • 643
  • 2
  • 8
  • 17