GORMのクエリ
追記:2009/02/10 grails 1.1 で autoImportに対応してもらえました(^^。 static mapping = { autoImport false } [jira] (GRAILS-2596)
GORM を使って記述したドメインクラスには、クエリの為のダイナミックメソッドが起動時に付与されます。
この「起動時に付与する」処理を行っているのは、 HibernateGrailsPlugin クラスです。このクラスは名前の通り Grails のプラグインの一つです。今回はクエリについて書きますので、プラグインの話はまたの機会にします。
GORMは、いくつかのクエリの手段を用意しています。
大きくは、
・ Dynamic Finders
・ Criteria
・ Hibernate Query Language(HQL)
に分かれますが、私は HQL を好んで利用しています。
Criteria と HQL のどちらを使うかは悩む所かもしれませんが、Criteria は可読性が良いように思えないし HQL でないと書けないクエリがあるので HQL を選んでいます。特に Groovy の場合は、HQLを使う場合でも動的にクエリを作りやすいですので Criteria の必要性を感じていません。
ただし、現在の Grails には、HQLを使った場合、やっかいな事があります。
それは、ドメインクラスに package を与えると、HQL で記述する際にpackage名を必ず付与しないといけない所です。これがあるが故に Criteria を選択しているという方もいらっしゃるかもしれません。
話を戻すと、具体的には、com.hoge.DomainClassA という名前のドメインクラスがあったとすると、次のように書く必要があります。
def results = DomainClassA.findAll( "from com.hoge.DomainClassA as d where d.name=:name", [name:'hoge'])
これは次のようにすこし知恵を絞って書くこともできます。
def results = DomainClassA.findAll( "from ${DomainClassA.class.name} as d where d.name=:name", [name:'hoge'])
この方法を使えば、package名がややこしい時にはいくらか助かりますし、package名がとても長い場合には短くなります。
しかし、上で挙げた例では長くなってしまいました…。
ともあれ、短くなろうが長くなってしまおうが、個人的な趣味かもしれませんが、どうもすっきりしません。auto-import の設定が出来ればよいのですが、現在の Grails は、この設定ができないようです。
# 良く分かりませんが、JPA と絡むのでしょうか・・・。(JIRAには上げたのですが)
SpaceCardでは、Grailsが対応してくれることを祈りながら、DomainClassNameConverterというクラスで、回避しています。
もう一つクエリの話を書きますと、個人的には、HQL を記述できる findAll や executeQuery といったダイナミックメソッド、さらにトランザクションブロックを与える withTransaction が、どうもすっきりしません。
まず、findAll がすっきりしない理由ですが、これはクラスメソッドであるにも関わらずクラス名を記述しないといけない点です。
例えば、デフォルトパッケージに DomainClassA というドメインクラスがあった場合次のように記述できます。
def results = DomainClassA.findAll( "from DomainClassA as a where a.name=:name", [name:'hoge'])
上のコードでは、DomainClassA が二度現れます。これは何度みてもしっくりこないのです。
次に、executeQuery がしっくりこないのは、クラスメソッドでありながらまったく関係ないドメインクラスをフェッチできてしまう点です。
例えば、デフォルトパッケージに DomainClassA と DomainClassB というドメインクラスがあった場合、次のように記述できます。
def results = DomainClassA.executeQuery( "select b from DomainClassB as b where b.name=:name", [name:'hoge'])
どうも変です。
withTransaction がしっくりこないのは、executeQuery と同じ理由です。
デフォルトパッケージに DomainClassA と DomainClassB というドメインクラスがあった場合、次のように記述できます。
DomainClassA.withTransaction { tx -> // id が 1 である DomainClassB を get DomainClassB b = DomainClassB.get(1) b.name = 'hoge' b.save() }
やはり、どうも変です…。変さ加減がお分かりになるでしょうか。
上で挙げたどうもしっくりこない部分は、初学者の学習を妨げる要因になりえると思うし、慣れてしまうのも如何なものかと思ってしまいます。
継承関係の中でのイベントの扱い
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の継承関係を持ったドメインクラスはこの方法で記述しています。
もっとスマートな方法があれば良いのですが。