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:
| product | manufacturer |
| product_id | manufacturer_id |
| product_name | manufacturer_name |
| manufacturer_id |
| created_date | created_date |
| modified_date | modified_date |
| created_user | created_user |
| modified_user | modified_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