M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

MyBatisのEnumTypeHandlerにハマる

季節感というか日付感のない記事で恐縮ですが、正月とか元旦とか関係なくMyBatisのEnumTypeHandlerでハマったはなしです。例によって結論から述べましょう。

結論

MyBatisでSQLのパラメータとしてenumを渡し、SQLステートメント内で#{paramName.propertyName}形式の参照を行う場合、#{paramName.propertyName, typeHandler=org.apache.ibatis.type.ObjectTypeHandler}と指定して強引にEnumTypeHandlerの起動を抑制する必要がある。

(2016/01/03追記)もう1つの回避策は、問題のenumのプロパティのうち永続化に関わるものだけのgetterを宣言したインターフェースを用意してenumにこれを実装させておき、一方MyBatisのマッパーの側でもパラメータの型をこのインターフェースで宣言させるというもの。なお、この記事を書いてからMyBatisに限らずシリアライズ/デシリアライズに関わる各種のライブラリでこの問題は起こりうることに気が付きました。やはりenumの利用方法としてあまりベターなものではないのでしょう。。

背景

MyBatisとenumというテーマでGoogle先生に質問すると、文字列ベースのマッピングを行うか数値(位置)ベースのマッピングを行うかうんぬんの記事などが見つかりますが、いずれにせよ共通しているのはenumenumのためハンドラーによってマッピングさせたいというものです。当たり前ですね。

これに対して私の問題関心は、enumをふつうのオブジェクトと同じようにマッピングさせたい、つまりenumを文字列や数値として扱うのではなくidやnameなど各種プロパティを持つふつうのクラスのインスタンスとしてマッピングさせたいというものでした。そうすることで一種のマスターをまずJavaの世界でenumとして定義しそれをアプリケーションの初期化ロジックの中でDBの世界でINSERTすることで、2つの世界に一貫性を持たせようとしたわけです。

ところが良くも悪くもenumのパラメータを指定するとデフォルトでEnumTypeHandlerが利用されてしまいenumインスタンスの名前ベースの値マッピングが行われてしまうのです。結果的に「IntegerはEnumにキャストできない」うんぬんのかなり状況を読みにくいエラーが発生してしまいます。SQL内で#{paramName.propertyName}形式の参照をすることができないのです。

あれこれ悩んだ末の解決策が以下のような記法でした。恐ろしく読みにくいですがこれできちんとプロパティ参照ができるようになりました(FooEnumenumであるとします):

public interface FooEnumMasterMapper {
	@Insert("INSERT INTO foo_master (id, name) "
	+ "VALUES (#{foo.id, typeHandler=org.apache.ibatis.type.ObjectTypeHandler}, "
	+ "#{foo.name, typeHandler=org.apache.ibatis.type.ObjectTypeHandler}) ")
	int insert(@Param("foo") FooEnum foo);
}

本当はこのような強引な上書きではなく、EnumTypeHandlerの利用の直接的抑制や、一連の登録済みハンドラからのEnumTypeHandlerの除去、EnumTypeHandlerを上書きしつつ実装はObjectTypeHandlerに全面的な移譲を行う独自ハンドラの提供などで対応したいところなのですが、調べもののための時間と体力と気力がないためこの方法で急場凌ぎとしました。