iBATIS の TypeHandlerCallback で getter.wasNull() するときの注意点など
iBATIS*1には、データベース型とエンティティの型を独自にカスタムしたい場合、いわゆるJDBCの自然なマッピングではない型を使用したいというニーズのために TypeHandler の拡張ポイントである TypeHandlerCallback を提供しています。特定のJava型*2にDBのカラム値をマップさせたい場合には、
- マップさせたいJava型について、TypeHandlerCallback をインプリメントしたクラスを書く
- 書いた TypeHandlerCallback を iBATIS の sqlmap-config.xml に登録する
という手順を踏むだけで、あとは iBATIS がよろしくやってくれるという便利機能です。これを利用すれば、DB側では VARCHAR で持っている区分値を Java 側では Enum で対応させたい、なんていうニーズにも対応できます。*3
で、今回のネタはといいますと。
先日その TypeHandlerCallback を使ったときに少しハマりまして。最終的に PostgreSQL JDBCドライバのソースまで追っかけていってなんとか解決したのですが、せっかくなのでその原因をbloggedして共有しておこうかなというものです。
問題の TypeHandlerCallback はこんな内容。本題と関係ない部分は適宜省略してます。
public class VarcharToBooleanTypeHandler implements TypeHandlerCallback { @Override public void setParameter(ParameterSetter setter, Object param) throws SQLException { // do something... } @Override public Object getResult(ResultGetter getter) throws SQLException { if (getter.wasNull()) { return null; } return getter.getString().equals("0"); } @Override public Object valueOf(String s) { return s; } }
一見、何の問題もなさそうに見えるのですが。これを設定ファイルに
正解はこうです。
public class VarcharToBooleanTypeHandler implements TypeHandlerCallback { @Override public void setParameter(ParameterSetter setter, Object param) throws SQLException { // do something... } @Override public Object getResult(ResultGetter getter) throws SQLException { String value = getter.getString(); if (getter.wasNull()) { return null; } return value.equals("0"); } @Override public Object valueOf(String s) { return s; } }
どこが違うのか? getResult メソッドの 中身がポイントです。
- getter.getString() して、その後で
- getter.wasNull()
しています。この順番が重要なのです。
では、この getter.wasNull() はいったい何をしているのでしょうか。実装を見てみましょう。例によって、関係ない部分は適宜省略してます。
public class ResultGetterImpl implements ResultGetter { private ResultSet rs; ... public boolean wasNull() throws SQLException { return rs.wasNull(); } ... }
どうやら、単に ResultSet#wasNull() に委譲しているだけのようです。では、 ResultSet#wasNull() の仕様はどうなっているのでしょうか。java.sql.ResultSet の Javadoc を紐解いてみますと……。
最後に読み込まれた列の値が SQL NULL であるかどうかを通知します。最初に列の getter メソッドの 1 つを呼び出してその値を読み込み、次に wasNull メソッドを呼び出して読み込まれた値が SQL NULL かどうかを判定する必要があります。
http://java.sun.com/javase/ja/6/docs/ja/api/java/sql/ResultSet.html#wasNull()
というわけです。wasNull の動作として、まず前段に何らかの getter メソッド(getString でも getInt でもなんでもいいです)が呼ばれて、その列の値が読み込まれた後でないと wasNull はきちんと動かないのですね。だから isNull じゃなくて wasNull なんだと。ここで私は初めて思い至ったわけです。*4勘のいい人なら wasNull というメソッド名を見ただけで感づいたのかもしれませんが……。
まとめ
以上、TypeHandlerCallback の実装で少しハマってしまった、というお話でした。TypeHandler 自体は、正しく使えばすごーく便利なので皆さんもっと積極的に使えばいいと思います。区分値をEnum管理すると精神衛生上にも良いですよ。