概要
ID を持ちまわる際、通常なら int, long, String などに格納するわけだが、これだと下記のような場合にちぐはぐになって分かりづらくなる可能性がある。
public someMethod(int hogeId, int fugaId) { ... }
これをこうできればケアレスミスを防げる。(ちぐはぐにするとIDEがエラーとなって教えてくれる。)
public someMethod(HogeId hogeId, FugaId fugaId) { ... }
注意ポイント
これは分かりやすくなる一方で、気を付けるポイントがある。
- Jackson で JSON に/から変換可能であること
- MyBatis でパラメータ/結果として扱えること
- Spring MVC の URL パラメータや
@RestController
の 戻り値 として扱えること - Id のクラスを沢山作ることになるので個々には面倒な記載が要らないこと(親クラスに実装もたせる)
- パフォーマンスが損なわれないこと
結論
下記で実現できる。
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 への変換
下記のいずれか:
- デフォルトコンストラクタ+setters
-
@JsonCreator
つきコンストラクタ - 合致する引数付きコンストラクタ
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 の定義が必要。
ただし、[2020/02/15追記] TypeHandler に具象クラスを列挙することで new できるようにする。setNonNullParameter
のみ実装する。getNullableResult
は具象クラスが解らず new できないので使えない。
MyBatisのResultMapでObject化できるように - [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 の負荷があるから結局トントン?
検証コード
// 後で書く