Java
MyBatis

[Java]NULL値を許容する整数型カラムを扱うTypeHandlerでハマる

結論

java.sql.ResultSet の動作をちゃんと押さえておきましょう。

うっかりハマった実装

NULL値 を許容する整数型カラム(JdbcType=INTEGER)を扱うTypeHandler を作成していて、NULL値 で insert したレコードを select した結果が 0 になってしまう事象に悩まされること小一時間。

HogeTypeHandler.java
public class HogeTypeHandler extends BaseTypeHandler<String> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, toInteger(parameter));
    }
    @Override
    public String getNullableResult(ResultSet rs, String columnName) therows SQLException {
        return toString(rs.getInt(columnName));
    }
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) therows SQLException {
        return toString(rs.getInt(columnIndex));
    }
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) therows SQLException {
        return toString(cs.getInt(columnIndex));
    }
    private String toString(Integer parameter) {
        // omitted
    }
    private Integer toInteger(String parameter) {
        // omitted
    }
}

デバッグで追ってみると、insert の際は直接 SQL 文にバインドされてレコード上は NULL値 となっているのに対し、select 時は TypeHandler が呼び出され rs.getInt(columnName)0 を返してくる???

ResultSet (Java Platform SE 8) の getInt メソッド

このResultSetオブジェクトの現在行にある指定された列の値を、Javaプログラミング言語のintとして取得します。
...
戻り値:
列値。値がSQL NULLの場合、返される値は0

ぐはっ、戻り値は Integer ではなく int だと。。。
(しかも唾棄すべき無意味な オートボクシング までさせていたとは。。。)

期待通りに動いた実装

FugaTypeHandler.java
public class FugaTypeHandler extends BaseTypeHandler<String> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setBigDecimal(i, toBigDecimal(parameter));
    }
    @Override
    public String getNullableResult(ResultSet rs, String columnName) therows SQLException {
        return toString(rs.getBigDecimal(columnName));
    }
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) therows SQLException {
        return toString(rs.getBigDecimal(columnIndex));
    }
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) therows SQLException {
        return toString(cs.getBigDecimal(columnIndex));
    }
    private String toString(BigDecimal parameter) {
        // omitted
    }
    private BigDecimal toBigDecimal(String parameter) {
        // omitted
    }
}

ResultSet (Java Platform SE 8) の getBigDecimal メソッド

このResultSetオブジェクトの現在行にある指定された列の値を、完全な精度のjava.math.BigDecimalとして取得します。
...
戻り値:
全精度の列値。値がSQL NULLの場合、返される値はJavaプログラミング言語のnull

BigDecimal で扱うので、(今回の要件だった)文字列化したときに小数点以下の値が付いてしまうことを危惧して BigDecimal#stripTrailingZeros() で精度調整するようにしてみたけれど、整数値を渡した場合の scale0 にしてくれるみたいです。

(2018/6/15 追記)

@pale2f に教えていただいた ResultSet#wasNull() を使って、実装を見直してみました。
ちなみに PreparedStatement#setInt() では NULL値 を設定できないので PreparedStatement#setNull() との使い分けが必要になりましたが、BigDecimal が出てこなくなってスッキリしました。

より良いと思われる実装

PiyoTypeHandler.java
public class PiyoTypeHandler extends BaseTypeHandler<String> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        if (parameter != null && !parameter.equals("")) {
            ps.setInt(i, Integer.parseInt(parameter));
        } else {
            ps.setNull(i, Types.INTEGER);
        }
    }
    @Override
    public String getNullableResult(ResultSet rs, String columnName) therows SQLException {
        int value = rs.getInt(columnName);
        return toString(value, rs.wasNull());
    }
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) therows SQLException {
        int value = rs.getInt(columnIndex);
        return toString(value, rs.wasNull());
    }
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) therows SQLException {
        int value = cs.getInt(columnIndex);
        return toString(value, cs.wasNull());
    }
    private String toString(int parameter, boolean wasNull) {
        if (wasNull) {
            return "";
        }
        return String.valueOf(parameter);
    }
}

(2018/6/15 追記 その2)

うっかりしてましたが、本事象は BaseTypeHandler<T> を素直に継承した場合は起こり得ません。
NULL値 だった場合に空文字列を返すという要件のために、以下の 3 メソッドをオーバーライドしてました。

オーバーライドしたメソッド
public T getResult(ResultSet rs, String columnName)
public T getResult(ResultSet rs, int columnIndex)
public T getResult(CallableStatement cs, String columnIndex)
元の実装
    // omitted
    if (rs.wasNull()) {
        return null;
    } else {
        return result;
    }
オーバーライドした実装
    // omitted
    return result;

つまり、元の実装ではちゃんと NULL値 かどうかの判定をしてくれていた
のに、自らそれを取り除いてしまったため、こんな捩れた実装になってしまったようですorz
(しかもここで ResultSet#wasNull() 出てきてるし。。。)