4
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-08

概要

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

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

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

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

注意ポイント

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

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

結論

下記で実現できる。

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); }
}
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 + ")";
    }
}
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} という記述で取り出せるようにしたい。
  • これは PiyoIdlong getId() というメソッドが定義されていれば良い。
2. IDクラスを文字するクラスを引数で受け取る場合
  • parameterType="com.~.Piyo" と指定して #{id} という記述で取り出せるようにしたい。
  • これには TypeHandler の定義が必要。 ただし、setNonNullParameter のみ実装する。getNullableResult は具象クラスが解らず new できないので使えない。 MyBatisのResultMapでObject化できるように [2020/02/15追記] TypeHandler に具象クラスを列挙することで new できるようにする。
  • [2020/02/15追記] Auto Generate された列を取得する際に下記だとなぜかうまくいかない。
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>
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 の負荷があるから結局トントン?
検証コード
// 後で書く
4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2