mybatisのtypehandlerでEnumな値をgetしたりsetしたり

やりたいこと

  • mybatisのtypehandlerを使ってみる

実行環境

  • 前回と同じ
  • コードも前回のものからの差分のみを記載する

前提

そもそもtypehandlerって何

  • Javaで定義されているクラスをJDBCクラスへのマッピング処理を行う
    • DBに値を格納する場合、Javaのクラスの値をDBで扱える型に変換するみたいなイメージ(正確な表現ではないと思う)
    • DBから値を取り出す場合はその逆

どういうケースで使用するか

  • JDBC側でBLOB型として定義している値と、Java側で byte[]型として定義している値の相互変換をするとか
  • Java側でenum型で定義している値の変換

検証

やってみたいこと

  • 独自で定義したenum型のためのtypehandlerを作成し、データの取得や格納で使用する

検証用データ作成

CREATE TABLE DIVISION (
  DIVISION_ID CHAR(2) NOT NULL,
  DIVISION_NAME VARCHAR(32) NOT NULL,
  PRIMARY KEY (DIVISION_ID)
);

INSERT INTO DIVISION (DIVISION_ID, DIVISION_NAME) VALUES ('10', '人事部');
INSERT INTO DIVISION (DIVISION_ID, DIVISION_NAME) VALUES ('20', '営業部');
INSERT INTO DIVISION (DIVISION_ID, DIVISION_NAME) VALUES ('30', '開発部');

CREATE TABLE STATUS (
  ID INTEGER NOT NULL,
  DESCRIPTION VARCHAR(32) NOT NULL,
  PRIMARY KEY (ID)
);

INSERT INTO STATUS (ID, DESCRIPTION) VALUES (0, 'アクティブ');
INSERT INTO STATUS (ID, DESCRIPTION) VALUES (9, 'ブロック');

CREATE TABLE USERS (
  USER_ID VARCHAR(32) NOT NULL,
  FIRST_NAME VARCHAR(32) NOT NULL,
  FAMILY_NAME VARCHAR(32) NOT NULL,
  DIVISION_ID CHAR(2) NOT NULL,
  STATUS INTEGER NOT NULL,
  PRIMARY KEY (USER_ID),
  FOREIGN KEY (DIVISION_ID) REFERENCES DIVISION (DIVISION_ID),
  FOREIGN KEY (STATUS) REFERENCES STATUS(ID)
);

INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME, DIVISION_ID, STATUS) VALUES ('hogefuga', 'ほげ', 'ふが', '10', 0);
INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME, DIVISION_ID, STATUS) VALUES ('foobar', 'ふー', 'ばー', '20', 9);

Userクラスの変更

  • Statusクラスを作成
public enum Status {
    ACTIVE(0), BLOCKED(9);

    private int id;

    Status(int id) {
        this.id = id;
    }
}
  • Userクラスにstatusフィールドを追加
--- a/core/src/main/java/com/github/nkiri/core/domain/model/user/User.java
+++ b/core/src/main/java/com/github/nkiri/core/domain/model/user/User.java
@@ -7,8 +7,8 @@ public class User {
   private final UserId userId;
   private final String firstName;
   private final String familyName;
-
   private Division division;
+  private Status status;
 
   public User(final UserId userId, final String firstName, final String familyName) {
     this.userId = userId;
@@ -18,7 +18,7 @@ public class User {
 
   public String toString() {
     return String.format(
-        "User {userId: %s, firstName: %s, familyName: %s, division: %s}",
-        userId, firstName, familyName, division);
+        "User {userId: %s, firstName: %s, familyName: %s, division: %s, status: %s}",
+        userId, firstName, familyName, division, status);
   }
 }

Statusを扱うtypehandlerを作成

  • BaseTypeHandlerインターフェースを実装する
  • とりあえずIDEが自動生成してくれるメソッドで記載
import com.github.nkiri.core.domain.model.user.Status;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

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

public class StatusTypeHandler extends BaseTypeHandler<Status> {
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, Status status, JdbcType jdbcType) throws SQLException {

    }

    @Override
    public Status getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return null;
    }

    @Override
    public Status getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return null;
    }

    @Override
    public Status getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return null;
    }
}

typehandlerの登録

  • mybatisがStatusを扱うときに、StatusTypeHandlerを使用するように設定する

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <typeHandlers>
    <typeHandler handler="com.github.nkiri.core.infrastructure.datastore.typehandler.StatusTypeHandler" javaType="com.github.nkiri.core.domain.model.user.Status"/>
  </typeHandlers>
</configuration>

applicationContext.xml

diff --git a/core/src/main/resources/applicationContext.xml b/core/src/main/resources/applicationContext.xml
index dca3030..770d657 100644
--- a/core/src/main/resources/applicationContext.xml
+++ b/core/src/main/resources/applicationContext.xml
@@ -28,6 +28,7 @@
 
   <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
     <property name="dataSource" ref="dataSource"/>
+    <property name="configLocation" value="classpath:mybatis-config.xml"></property>
   </bean>
 
   <mybatis:scan base-package="com.github.nkiri.core.infrastructure.datastore"/>

StatusTypeHandlerの実装

実装するべきメソッドについて

getNullableResult

Statusクラスにメソッドを追加

diff --git a/core/src/main/java/com/github/nkiri/core/domain/model/user/Status.java b/core/src/main/java/com/github/nkiri/core/domain/model/user/Status.java
index 0a24ebb..8866d56 100644
--- a/core/src/main/java/com/github/nkiri/core/domain/model/user/Status.java
+++ b/core/src/main/java/com/github/nkiri/core/domain/model/user/Status.java
@@ -1,5 +1,7 @@
 package com.github.nkiri.core.domain.model.user;
 
+import java.util.Arrays;
+
 public enum Status {
     ACTIVE(0), BLOCKED(9);
 
@@ -8,4 +10,13 @@ public enum Status {
     Status(int id) {
         this.id = id;
     }
+
+    public int getId() {
+        return id;
+    }
+
+    // StatusTypeHandlerのgetNullableResultで使用する
+    public static Status getStatus(int id) {
+        return Arrays.stream(values()).filter(v -> v.getId() == id).findFirst().get();
+    }
 }

getNullableResultを実装

diff --git a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
index 03cb667..2b35e2f 100644
--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
@@ -17,16 +17,16 @@ public class StatusTypeHandler extends BaseTypeHandler<Status> {
 
     @Override
     public Status getNullableResult(ResultSet resultSet, String s) throws SQLException {
-        return null;
+        return Status.getStatus(resultSet.getInt(s));
     }
 
     @Override
     public Status getNullableResult(ResultSet resultSet, int i) throws SQLException {
-        return null;
+        return Status.getStatus(resultSet.getInt(i));
     }
 
     @Override
     public Status getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
-        return null;
+        return Status.getStatus(callableStatement.getInt(i));
     }
 }

実行してみた結果

  • mainメソッドは前回とか前々回と同じ感じで、hogefugafoobarのデータを取得する
  • 実行結果
User {userId: hogefuga, firstName: ほげ, familyName: ふが, division: Division {10:人事部}, status: ACTIVE}
User {userId: foobar, firstName: ふー, familyName: ばー, division: Division {20:営業部}, status: BLOCKED}

Process finished with exit code 0

setNonNullParameter

準備(ステータス更新処理を作っておく)

  • UserMapperにupdateメソッドを追加する
--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/UserMapper.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/UserMapper.java
@@ -3,8 +3,10 @@ package com.github.nkiri.core.infrastructure.datastore;
 import com.github.nkiri.core.domain.model.user.User;
 import com.github.nkiri.core.domain.model.user.UserId;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 @Mapper
 public interface UserMapper {
   User get(UserId userId);
+  int update(@Param("user") User user);
 }
--- a/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
+++ b/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
@@ -19,6 +19,13 @@
     ;
   </select>

+  <update id="update" parameterType="map">
+    UPDATE USERS SET STATUS = #{user.status}
+    <where>
+      USER_ID = #{user.userId.id}
+    </where>
+  </update>
+
   <resultMap id="userIdResultMap" type="com.github.nkiri.core.domain.model.user.UserId">
     <constructor>
  • UserRepositoryにもメソッドを追加する
--- a/core/src/main/java/com/github/nkiri/core/domain/model/user/UserRepository.java
+++ b/core/src/main/java/com/github/nkiri/core/domain/model/user/UserRepository.java
@@ -6,4 +6,5 @@ import org.springframework.stereotype.Repository;
 public interface UserRepository {
 
   User get(UserId userId);
+  void update(User user);
 }
  • UserクラスにStatusのsetterメソッドを追加
--- a/core/src/main/java/com/github/nkiri/core/domain/model/user/User.java
+++ b/core/src/main/java/com/github/nkiri/core/domain/model/user/User.java
@@ -21,4 +21,8 @@ public class User {
         "User {userId: %s, firstName: %s, familyName: %s, division: %s, status: %s}",
         userId, firstName, familyName, division, status);
   }
+
+  public void setStatus(final Status status) {
+    this.status = status;
+  }
 }

setNonNullParameterの実装

--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
@@ -12,7 +12,7 @@ import java.sql.SQLException;
 public class StatusTypeHandler extends BaseTypeHandler<Status> {
     @Override
     public void setNonNullParameter(PreparedStatement preparedStatement, int i, Status status, JdbcType jdbcType) throws SQLException {
-
+        preparedStatement.setInt(i, status.getId());
     }

     @Override

テスト用の呼び出し処理

@Service
public class SampleApplicationService {

  private final UserRepository userRepository;

  public SampleApplicationService(final UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public void sample() {
    // 初期状態のデータを取得して表示
    User user1 = userRepository.get(new UserId("hogefuga"));
    System.out.println(user1);

    // statusを書き換えて保存
    user1.setStatus(Status.BLOCKED);
    userRepository.update(user1);

    // 更新後のデータを取得して表示
    User user1Updated = userRepository.get(new UserId("hogefuga"));
    System.out.println(user1Updated);

  }
}

実行してみた結果

  • ACTIVEだったものがBLOCKEDに書き変わっていることが確認できる
User {userId: hogefuga, firstName: ほげ, familyName: ふが, division: Division {10:人事部}, status: ACTIVE}
User {userId: hogefuga, firstName: ほげ, familyName: ふが, division: Division {10:人事部}, status: BLOCKED}

Process finished with exit code 0

まとめ

  • BaseTypeHandlerを使ってEnum型の変換を試した
  • mybatisにはEnumTypeHandlerというクラスがあるので、本来であればそっちを使うのが望ましい
  • 値をセットするときはPreparedStatement、値を取得するときはResultSetに対して、カラム名またはindexを指定して対象のデータにアクセスしているっぽいことがわかった