Code Voyeur
RSS
Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net

Audit Columns and ActiveRecord

One of the simplest ways to track changes to a table is to include audit columns that are populated automatically on create and update. When data access is performed with data access objects using stored procedures or string-literal parameterized SQL, it's easy to include default values for these columns. However, using ActiveRecord (and therefore NHibernate), a different strategy is required since SQL statements are dynamically generated.

For this example, consider the following basic data model:

productmanufacturer
product_idmanufacturer_id
product_namemanufacturer_name
manufacturer_id
created_datecreated_date
modified_datemodified_date
created_usercreated_user
modified_usermodified_user

Each table contains a set of audit columns (created_date, modified_date, created_user and modified_user). Created columns should be populated on insert only and each update should trigger the modified columns to be populated. A solution using ActiveRecord is to create a common base class with audit properties that will be extended by classes needing these audit column mappings.

The first step step is to create the base class - ActiveRecordAuditBase.

[ActiveRecord]
    public abstract class ActiveRecordAuditBase<T> : ActiveRecordBase<T>
    {        
        private string _createdUser;

        [Property("created_user", Update=false, Access=PropertyAccess.NosetterCamelcaseUnderscore)]
        public string CreatedUser
        {
            get { return _createdUser; }            
        }

        private string _modifiedUser;

        [Property("modified_user", Insert = false, Access = PropertyAccess.NosetterCamelcaseUnderscore)]
        public string ModifiedUser
        {
            get { return _modifiedUser; }
        }

        private DateTime _createdDate;

        [Property("created_date", Update = false, Access = PropertyAccess.NosetterCamelcaseUnderscore)]
        public DateTime CreatedDate
        {
            get { return _createdDate; }            
        }

        private DateTime? _modifiedDate;

        [Property("modified_date", Insert = false, Access = PropertyAccess.NosetterCamelcaseUnderscore)]
        public DateTime? ModifiedDate
        {
            get { return _modifiedDate; }
        }  
    ...
}
ActiveRecordAuditBase extends ActiveRecordBase (using generics in this case) making it a viable base class for other ActiveRecord classes. Otherwise, it is a somewhat standard AR class with four properties mapping to four columns. However, there are a couple of atypical attribute parameter settings to notice.

Because this example works with the assumption that the audit columns should be automatically populated and not controlled by the AR class consumer, properties have accessors only. ActiveRecord needs to be informed that these properties are readonly, which is the purpose of PropertyAccess.NosetterCamelcaseUnderscore in the Property attribute. With the PropertyAccess parameter set in this way, ActiveRecord will attempt to find a field named the same as the property, but camel-cased with a leading underscore.

This article originally proposed a solution in which the values for the audit columns were set automatically within the property accessors. The problem is that by default NHibernate automatically flushes (tries to persist the instance) when an instance is dirty.

The reason an ActiveRecordAuditBase instance would look dirty is because the ModifiedUser and ModifiedDate properties were never being populated by what was stored in the database. The accessors for these properties were automatically setting the values to the current user and timestamp. A dirty instance was guaranteed. Whether the saved record had a date_modified that was null or any time other than now, it would differ from the automatic DateTime.Now that was set at load.

Old version:

get { return (_modifiedDate = DateTime.Now); }

A better solution is to override the Update, Insert and Save methods in the ActiveRecordAuditBase class. The audit properties are set when these commit methods are called. The audit columns are still able to be readonly. The flush methods are also overridden. See the sample project for full details.


...

public override void Update()
{
    auditUpdate();
    base.Update();
}

public override void Create()
{
    auditCreate();
    base.Create();
}

public override void Save()
{
    auditCreate();
    auditUpdate();
    base.Save();
}

...

private void auditUpdate()
{
    _modifiedDate = DateTime.Now;
    _modifiedUser = Environment.UserName;
}

private void auditCreate()
{
    _createdDate = DateTime.Now;
    _createdUser = Environment.UserName;
}

...

The other parameter that should be set on the AR Property attributes is Insert = false for modified columns and Update = false for created columns. These settings will prevent AR from trying to use the modified columns on insert and created columns on update.

Any class that needs these auto-populating default columns now simply needs to extend the auditable base class.

[ActiveRecord("products")]    
public class Product : ActiveRecordAuditBase<Product>    
{     

    private int _id;        
    
    [PrimaryKey(PrimaryKeyType.Identity, "product_id")]     
    public int Id     
    {      
        get { return _id; }      
        set { _id = value; }     
    }        
    
    private string _name;        
    
    [Property("product_name")]     
    public string Name     
    {      
        get { return _name; }      
        set { _name = value; }     
    }        
    
    private Manufacturer _manufacturer;        
    
    [BelongsTo("manufacturer_id")]     
    public Manufacturer Manufacturer     
    {      
        get { return _manufacturer; }      
        set { _manufacturer = value; }     
    }     
}
Creating and updating products and manufacturers requires no additional code for capturing the audit data.
Manufacturer m = new Manufacturer();                  
m.Name = "Roland";                  
m.Create(); 
                   
Product p = new Product();                  
p.Manufacturer = m;                  
p.Name = "Juno G";                  
p.Create();

//create a different date updated                  
System.Threading.Thread.Sleep(2000);                    
p.Name = "Juno D 61 Key Pro Keyboard";                  
p.Update();

Download Sample Project

References

Sample project source code
Article Posted: Sunday, December 09, 2007

Leave a Comment


Contact Code Voyeur about this article.