ID を格納する変数を int/long でなく ID 型にする (Java)

# 概要

ID を持ちまわる際、通常なら int, long, String などに格納するわけだが、これだと下記のような場合にちぐはぐになって分かりづらくなる可能性がある。

```java
public someMethod(int hogeId, int fugaId) { ... }
```

これをこうできればケアレスミスを防げる。（ちぐはぐにするとIDEがエラーとなって教えてくれる。）

```java
public someMethod(HogeId hogeId, FugaId fugaId) { ... }
```

#### 注意ポイント

これは分かりやすくなる一方で、気を付けるポイントがある。

1. Jackson で JSON に/から変換可能であること
2. MyBatis でパラメータ/結果として扱えること
3. Spring MVC の URL パラメータや `@RestController` の 戻り値 として扱えること
4. Id のクラスを沢山作ることになるので個々には面倒な記載が要らないこと(親クラスに実装もたせる)
5. パフォーマンスが損なわれないこと

#### 結論

下記で実現できる。

```java:PiyoId.java
package your.package;

import your.package.LongId;
import lombok.EqualsAndHashCode;

@EqualsAndHashCode(callSuper = true)
class PiyoId extends LongId {
    public PiyoId(String id) { super(id); }
    public PiyoId(long id) { super(id); }
}
```

```java:LongId.java
package your.package;

import com.fasterxml.jackson.annotation.JsonValue;
import lombok.EqualsAndHashCode;

@EqualsAndHashCode
public abstract class LongId {
    private final long id;

    public LongId(String id) {
        this.id = Long.parseLong(id);
    }

    public LongIdBase(long id) {
        this.id = id;
    }

    @JsonValue
    public long getId() {
        return this.id;
    }

    public String toString() {
        return this.getClass().getSimpleName() + "(" + this.id + ")";
    }
}
```

```java:LongIdTypeHandler.java
package your.package;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import your.package.HogeId;
import your.package.LongId;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;

// ここにIDクラスを列挙する
@MappedTypes(value = {HogeId.class})
public class LongIdTypeHandler<E extends LongId> extends BaseTypeHandler<E> {
    private final Class<E> type;

    public LongIdTypeHandler(Class<E> type) {
        if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
    }

    private E createInstance(boolean wasNull, long id) {
        if (wasNull) {
            return null;
        }
        try {
            return type.getDeclaredConstructor(long.class).newInstance(id);
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int num, E parameter, JdbcType jdbcType) throws SQLException {
        long id = rs.getLong(columnName);
        return createInstance(rs.wasNull(), id);
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        long id = rs.getLong(columnName);
        return createInstance(rs.wasNull(), id);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        long id = rs.getLong(columnIndex);
        return createInstance(rs.wasNull(), id);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        long id = cs.getLong(columnIndex);
        return createInstance(cs.wasNull(), id);
    }
}
```

# 詳細

## Jackson 対応

シームレスに変換できるためには次のいずれかである必要があります。

#### JSON -> Object への変換

下記のいずれか:
1. デフォルトコンストラクタ＋setters
2. `@JsonCreator` つきコンストラクタ
3. 合致する引数付きコンストラクタ

#### Object -> JSON への変換

下記のいずれか:
4. getters から判断
5. `@JsonValue` つきのメソッド実行

#### 結論

3と5に対応させる

## MyBatis 対応

#### Object を MyBatisパラメータ で渡せるように

下記の両方に対応する必要がある:

##### 1. IDクラスがパラメータである場合

- `parameterType="com.～.PiyoId"` と指定して `#{id}` という記述で取り出せるようにしたい。
- これは `PiyoId` に `long getId()` というメソッドが定義されていれば良い。

##### 2. IDクラスを文字するクラスを引数で受け取る場合

- `parameterType="com.～.Piyo"` と指定して `#{id}` という記述で取り出せるようにしたい。
- これには TypeHandler の定義が必要。 ~~ただし、`setNonNullParameter` のみ実装する。`getNullableResult` は具象クラスが解らず new できないので使えない。
MyBatisのResultMapでObject化できるように~~ [2020/02/15追記] TypeHandler に具象クラスを列挙することで new できるようにする。
- [2020/02/15追記] Auto Generate された列を取得する際に下記だとなぜかうまくいかない。

```xml:selectKeyだとうまくいかない
<insert id="insert" parameterType="your.package.Hoge">
  INSERT INTO hoges (value1, value2)
  VALUES (#{value1}, #{value2})
  <selectKey resultType="your.package.HogeId" keyProperty="id" order="AFTER">
    select @@IDENTITY
  </selectKey>
</insert>
```

```xml:useGeneratedKeysならうまくいく
<insert id="insert" parameterType="your.package.Hoge" useGeneratedKeys="true" keyProperty="id">
  INSERT INTO hoges (value1, value2)
  VALUES (#{value1}, #{value2})
</insert>
```

## Spring 対応

#### Spring MVC の `Controller` のパラメータ

- 下記のケースのいずれであっても 123 のような数値で来たパラメータを `PiyoId` 型として受け取りたい。
  - `@PathVariable("piyoId") PiyoId piyoId` とした場合
  - `@RequestBody PiyoForm form` や `@ModelAttribute PiyoForm form` として受けとる場合で、`form` の中で `PiyoId` 型で受け取る場合
- 通常ならエラー: `Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'your.package.PiyoId': no matching editors or conversion strategy found`
- 結論: IDクラスに String を受け取るコンストラクタがあればできる。

#### Spring MVC の `@RestController` の戻り値

- `PiyoId` が混ざっていたら数値として JSON 化したい。
- 結論: これは前述の Jackson 対応していれば大丈夫。 

## パフォーマンスが損なわれないこと

- ID 作るのに new が必要になったり、ハッシュ値計算が複雑になるケースがあったが、1000万回回しても両者ともに 1.5 秒程度でほぼ差はなし。long 型だと Boxing の負荷があるから結局トントン？

```java:検証コード
// 後で書く
```