NHibernateでデータベースに永続化する際、レコードの作成日や更新日を自動的に挿入する


# 概要

データベースのレコードにCreatedAt（作成日）やUpdatedAt（更新日）などのフィールドを持たせることがあるかと思います。

[NHibernate](http://nhibernate.info/)にはレコードの挿入や更新などのイベント発生時に指定した処理をさせる機構があるので、それを活用することでCreatedAtやUpdatedAtなどのフィールドを自動的に更新させることができます。

# 実装方針

挿入・更新などのイベントハンドラは、`IPreInsertEventListener`や`IPreUpdateEventListener`を継承したクラス内に記述します。
そのイベントハンドラクラスのインスタンスをNHibernate`Configuration.EventListeners`の
`PreInsertEventListeners`や`PreUpdateEventListeners`に登録することで、イベントハンドラが呼ばれるようになります。

作成日や更新日を保存するのに利用するプロパティは独自のCustom Attributeを設定することで指定します。


# 実際のコード

## Custom Attribute

挿入・更新時に自動的に値を設定するフィールドをマークアップするのに利用するCustom Attributeです。

```csharp
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class AutofillCurrentDateTimeOnInsertAttribute : Attribute
{
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class AutofillCurrentDateTimeOnUpdateAttribute : Attribute
{
}
```

## イベントハンドラ

上で定義した`AutofillCurrentDateTimeOnInsertAttribute` `AutofillCurrentDateTimeOnUpdateAttribute`が設定されているプロパティで、かつ、DateTimeOffset型のプロパティだったとき、それらのプロパティの値を自動的に設定するように記述しています。

```csharp
public class AutofillCurrentDateTimeEventListener : IPreUpdateEventListener, IPreInsertEventListener
{
	public void Register(Configuration configuration)
	{
	    configuration.SetListener(ListenerType.PreInsert, this);
	    configuration.SetListener(ListenerType.PreUpdate, this);
	}

	public bool OnPreInsert(PreInsertEvent @event)
	{
	    var now = DateTimeOffset.Now;

	    @event.Entity.GetType().GetProperties()
	        .Where(_ => Attribute.IsDefined(_, typeof(AutofillCurrentDateTimeOnInsertAttribute)) && _.PropertyType == typeof(DateTimeOffset))
	        .ToList()
	        .ForEach(prop => {
	            Set(@event.Persister, @event.State, prop.Name, now);
	            prop.SetValue(@event.Entity, now);
	        });
	    

	    return false;
	}

	public bool OnPreUpdate(PreUpdateEvent @event)
	{
	    var now = DateTimeOffset.Now;

	    @event.Entity.GetType().GetProperties()
	        .Where(_ => Attribute.IsDefined(_, typeof(AutofillCurrentDateTimeOnUpdateAttribute)) && _.PropertyType == typeof(DateTimeOffset))
	        .ToList()
	        .ForEach(prop => {
	            Set(@event.Persister, @event.State, prop.Name, now);
	            prop.SetValue(@event.Entity, now);
	        });

	    return false;
	}

	//ref: http://stackoverflow.com/a/24908880
	private void Set(IEntityPersister persister, object[] state, string propertyName, object value)
	{
	    var index = Array.IndexOf(persister.PropertyNames, propertyName);
	    if (index == -1)
	        return;
	    state[index] = value;
	}
}
```

## イベントハンドラの登録

Fluent NHibernateの場合、次のような形で登録することになると思います。

```csharp

sessionFactory = Fluently.Configure()
	.Database(
	    SQLiteConfiguration.Standard
	        .InMemory()
	        .ShowSql()
	    )
	.Mappings(m =>
	{
	    m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly());
	})
	.ExposeConfiguration(cfg => {
	    cfg.EventListeners.PreInsertEventListeners = new IPreInsertEventListener[] {
	        new AuditEventListener()
	    };
	    cfg.EventListeners.PreUpdateEventListeners = new IPreUpdateEventListener[] {
	        new AuditEventListener()
	    };
	    configuration = cfg;
	})
	.BuildSessionFactory();
```

## モデル側で自動設定対象となるプロパティにAttributeでマークアップする

モデル側では上で定義したAttributeを付与するだけです。
例えば、以下の例では、`CreatedAt`はレコードが作成されたとき、`UpdatedAt`はレコード作成と更新があったときにNHibernateによって自動的に現在時刻が設定されるようになります。

```csharp
public class User
{
	public virtual UserId UserId { get; protected internal set; }

	[AutofillCurrentDateTimeOnInsert]
	public virtual DateTimeOffset CreatedAt { get; protected internal set; }

	[AutofillCurrentDateTimeOnInsert, AutofillCurrentDateTimeOnUpdate]
	public virtual DateTimeOffset UpdatedAt { get; protected internal set; }
}
```


# 備考

今回はCustom Attributeでマークアップすることで対象のプロパティを特定していましたが、『ITimestampableなど独自インターフェイスを実装しているクラスだけ対象にする』『CreatedAtという名前のプロパティを対象にする』というような実装も可能です。