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先生に質問すると、文字列ベースのマッピングを行うか数値(位置)ベースのマッピングを行うかうんぬんの記事などが見つかりますが、いずれにせよ共通しているのはenum
をenum
のためハンドラーによってマッピングさせたいというものです。当たり前ですね。
これに対して私の問題関心は、enum
をふつうのオブジェクトと同じようにマッピングさせたい、つまりenum
を文字列や数値として扱うのではなくidやnameなど各種プロパティを持つふつうのクラスのインスタンスとしてマッピングさせたいというものでした。そうすることで一種のマスターをまずJavaの世界でenum
として定義しそれをアプリケーションの初期化ロジックの中でDBの世界でINSERTすることで、2つの世界に一貫性を持たせようとしたわけです。
ところが良くも悪くもenum
のパラメータを指定するとデフォルトでEnumTypeHandler
が利用されてしまいenum
インスタンスの名前ベースの値マッピングが行われてしまうのです。結果的に「IntegerはEnumにキャストできない」うんぬんのかなり状況を読みにくいエラーが発生してしまいます。SQL内で#{paramName.propertyName}
形式の参照をすることができないのです。
あれこれ悩んだ末の解決策が以下のような記法でした。恐ろしく読みにくいですがこれできちんとプロパティ参照ができるようになりました(FooEnum
はenum
であるとします):
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
に全面的な移譲を行う独自ハンドラの提供などで対応したいところなのですが、調べもののための時間と体力と気力がないためこの方法で急場凌ぎとしました。