継承関係の中でのイベントの扱い

GORM は、オブジェクトの生成・更新・削除・読込み(ロード)に関するイベント処理をドメインクラスに適切なクロージャを記述することで登録する事ができます。但しそれぞれについて直前・直後の両方をもれなく扱える分けではなく、次の表の通りです。

イベント 記述するクロージャ
新規にオブジェクトを DB に保存する直前のイベント beforeInsert
DBに存在するオブジェクトを更新する直前のイベント beforeUpdate
DBに存在するオブジェクトを削除する直前のイベント beforeDelete
DBに存在するオブジェクトを読込んだ直後のイベント onLoad

Hibernate Events Pluginを使うと前後もれなく扱えるようになります。

記述例は次のようになります。
Grailsのリファレンスから引用:

class Person {
   Date dateCreated
   Date lastUpdated
   def beforeInsert = {
       dateCreated = new Date()
   }
   def beforeUpdate = {
       lastUpdated = new Date()
   }
}

この例は、Person クラスのオブジェクトに対して、次の二つの処理を行っています。
・ 新規に生成したオブジェクトを DB に保存する直前に dateCreated という Date 型の属性にその処理時点での Date 値を設定する
・ オブジェクトを更新する直前に lastUpdated という Date 型の属性にその処理時点での Date 値を設定する(オブジェクトの更新が行われる度に Date 値の更新を行っている)

ここで、次のような二つのドメインクラスが存在した場合を考えてみます。

class A {
   Date dateCreated
}

class B extends A {
   Date lastUpdated
}

クラスBは、クラスAを継承しています。
クラスAは、生成された時点での Date 値のみを属性として扱うクラスです。
クラスBは、生成と更新、両時点での Date 値を属性として扱うクラスです。

これらの属性をイベント処理で扱う場合、次のように記述する方法を思いつかれるかもしれません。

class A {
   Date dateCreated
   Date lastUpdated 
   def beforeInsert = {
       dateCreated = new Date()
   }
}

class B extends A {
   Date lastUpdated
   def beforeUpdate = {
       lastUpdated = new Date()
   }
}

このコードは上手くイベント処理が出来ます。

少し発展させてみます。
クラスAとクラスBが、次のようなドメインクラスであったとします。

class A {
   Date dateCreated
   Date lastUpdated
}

class B extends A {
   String ipAddress
}

クラスAは、生成と更新、両時点での Date 値を属性として扱うクラスです。
クラスBは、生成と更新、両時点での利用者のIPアドレスを属性として扱うクラスです。

これらの属性をイベント処理で扱う場合、次のコードではクラスBは正常にdateCreated と lastUpdatedのDate 値が設定されません。
※ RequestUtils.getIp() は、利用者のIPアドレスを取得できるものとします。

class A {
   Date dateCreated
   Date lastUpdated
   def beforeInsert = {
       dateCreated = new Date()
   }
   def beforeUpdate = {
       lastUpdated = new Date()
   }
}

class B extends A {
   String ipAddress
   def beforeInsert = {
       ipAddress = RequestUtils.getIp()
   }
   def beforeUpdate = {
       ipAddress = RequestUtils.getIp()
   }
}

DBを覗いてみると、クラスBの dateCreated と lastUpdated には Date 値ではなく null 値が設定されます。
こうなってしまうのは、サブクラスでスーパークラスと同じ名前のクロージャを記述した場合、サブクラス側の処理においてスーパークラスクロージャは呼び出されないからです。

これをどのように解決するか考えた結果、次のような方法を思いつきました。

class A {
   Date dateCreated
   Date lastUpdated
   def beforeInsert = {
       onBeforeInsert(delegate)
   }
   def beforeUpdate = {
       onBeforeUpdate(delegate)
   }
   protected def onBeforeInsert(obj) {
       obj.dateCreated = new Date()
   }
   protected def onBeforeUpdate(obj) {
       obj.lastUpdated = new Date()
   }
}

class B extends A {
   String ipAddress
   protected def onBeforeInsert(obj) {
       super.onBeforeInsert(obj)
       obj.ipAddress = RequestUtils.getIp()
   }
   protected def onBeforeUpdate(obj) {
       super.onBeforeUpdate(obj)
       obj.ipAddress = RequestUtils.getIp()
   }
}

この方法の場合、クラスBはクラスAのクロージャがそのまま呼び出されます。
そして、クラスBのonBeforeInsert(obj) が呼び出されるとクラスAの onBeforeInsert(obj) が呼び出され、その後自身が持つコードが処理されます。

このコードは、上手く動きます。

SpaceCardの継承関係を持ったドメインクラスはこの方法で記述しています。
もっとスマートな方法があれば良いのですが。