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

## 結論

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

## うっかりハマった実装

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

```java: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 メソッド](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/ResultSet.html#getInt-int-)

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

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

## 期待通りに動いた実装

```java: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 メソッド](https://docs.oracle.com/javase/jp/8/docs/api/java/sql/ResultSet.html#getBigDecimal-java.lang.String-)

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

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

（2018/6/15 追記）

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

## より良いと思われる実装

```java: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 メソッドをオーバーライドしてました。

``` java:オーバーライドしたメソッド
public T getResult(ResultSet rs, String columnName)
public T getResult(ResultSet rs, int columnIndex)
public T getResult(CallableStatement cs, String columnIndex)
```

``` java:元の実装
    // omitted
    if (rs.wasNull()) {
        return null;
    } else {
        return result;
    }
```

``` java:オーバーライドした実装
    // omitted
    return result;
```

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