springframeworkとmybatisでresultMapを使用したマッピング

やること

  • springframeworkとmybatisを使ったデータ取得
  • resultMapを使用したオブジェクトへのマッピング
    • 値オブジェクトを含むコンストラクタの扱い

やらないこと

  • springframeworkについての話
  • postgresについての話
  • assosiationやcollection(これはまた今度)

前提

  • Repositoryのメソッドを呼び出す処理については言及しない
    • 基本的には取得したデータを System.out.prinlnで出力したものを 実行結果として記載する

実行環境

  • Java8
  • springframework 5.3.23
  • mybatis 3.5.10
  • mybatis-spring 2.0.7
  • postgres14.5 (docker)
  • postgresql (sql driver) 42.5.0

構成

DB

CREATE TABLE USERS (
  USER_ID VARCHAR(32) NOT NULL,
  FIRST_NAME VARCHAR(32) NOT NULL,
  FAMILY_NAME VARCHAR(32) NOT NULL,
  PRIMARY KEY (USER_ID)
);
INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME) VALUES ('hogefuga', 'ほげ', 'ふが');
INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME) VALUES ('foobar', 'ふー', 'ばー');

Javaクラス

User

public class User {

  private final String userId;
  private final String firstName;
  private final String familyName;

  public User(final String userId, final String firstName, final String familyName) {
    this.userId = userId;
    this.firstName = firstName;
    this.familyName = familyName;
  }

  public String toString() {
    return String.format(
        "User {userId: %s, firstName: %s, familyName: %s}", userId, firstName, familyName);
  }
}

UserRepository

interface

import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository {

  User get(String userId);
}

impl

import com.github.nkiri.core.domain.model.user.User;
import com.github.nkiri.core.domain.model.user.UserRepository;
import org.springframework.stereotype.Repository;

@Repository
public class DefaultUserRepository implements UserRepository {

  private final UserMapper userMapper;

  public DefaultUserRepository(final UserMapper userMapper) {
    this.userMapper = userMapper;
  }

  public User get(String userId) {
    return userMapper.get(userId);
  }
}

UserMapper

  • 後述

applicationContext.xml

  • 省略

検証

文字列パラメータを渡してデータ取得

UserMapper

  • interfaceで定義した引数をxml側で参照する
import com.github.nkiri.core.domain.model.user.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
  User get(String userId);
}
  • resultMapの定義はtype属性にマッピングするクラスを指定するだけ
    • select要素のresultMap属性で定義した resultMapを指定する
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.github.nkiri.core.infrastructure.datastore.UserMapper">
  <select id="get" resultMap="userResultMap">
    SELECT
    USER_ID as userId
    , FIRST_NAME as firstName
    , FAMILY_NAME as familyName
    FROM USERS
    <where>
      USER_ID = #{userId}
    </where>
    ;
  </select>
  
  <resultMap id="userResultMap" type="com.github.nkiri.core.domain.model.user.User"/>
</mapper>

実行結果

User {userId: hogefuga, firstName: ほげ, familyName: ふが}

User.userIdを値オブジェクトとして定義する

変更点

UserId

public class UserId {
    private final String id;

    public UserId(final String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return id;
    }
}

User

index e05003d..93c1db8 100644
--- 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
@@ -2,11 +2,11 @@ package com.github.nkiri.core.domain.model.user;
 
 public class User {
 
-  private final String userId;
+  private final UserId userId;
   private final String firstName;
   private final String familyName;
 
-  public User(final String userId, final String firstName, final String familyName) {
+  public User(final UserId userId, final String firstName, final String familyName) {
     this.userId = userId;
     this.firstName = firstName;
     this.familyName = familyName;

実行結果(エラー)

  • DBから取得したデータをUserのコンストラクタに渡した際に型が不一致でエラーになってる模様
Exception in thread "main" org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Error instantiating class com.github.nkiri.core.domain.model.user.User with invalid types (UserId,String,String) or values (hogefuga,ほげ,ふが). Cause: java.lang.IllegalArgumentException: argument type mismatch

修正

  • DBのUSER_IDカラムから取得した値を UserIdクラスのコンストラクタに渡し、その結果を用いてUserクラスのコンストラクタを呼び出す
    • constructor要素の子要素には、Javaクラスに用意したコンストラクタの引数の順番に合わせて idArgarg要素を並べる必要がある
      • 今回は (UserId, String, String)の引数を持つコンストラクタが使用される
      • 引数を順不同としたい場合は name属性を指定する
index 7cc80d5..31b127f 100644
--- 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
@@ -14,5 +14,17 @@
     ;
   </select>
 
-  <resultMap id="userResultMap" type="com.github.nkiri.core.domain.model.user.User"/>
+  <resultMap id="userIdResultMap" type="com.github.nkiri.core.domain.model.user.UserId">
+    <constructor>
+      <idArg column="userId" javaType="string"/>
+    </constructor>
+  </resultMap>
+
+  <resultMap id="userResultMap" type="com.github.nkiri.core.domain.model.user.User">
+    <constructor>
+      <idArg resultMap="userIdResultMap" column="userId" javaType="com.github.nkiri.core.domain.model.user.UserId"/>
+      <arg column="firstName" javaType="string"/>
+      <arg column="familyName" javaType="string"/>
+    </constructor>
+  </resultMap>
 </mapper>
\ No newline at end of file

実行結果

User {userId: hogefuga, firstName: ほげ, familyName: ふが}

値オブジェクトをgetの引数として渡したい

変更点

UserRepository

  • interface
index ca26f43..28f9f39 100644
--- 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
@@ -5,5 +5,5 @@ import org.springframework.stereotype.Repository;
 @Repository
 public interface UserRepository {
 
-  User get(String userId);
+  User get(UserId userId);
 }
  • impl
index e288413..ae792da 100644
--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/DefaultUserRepository.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/DefaultUserRepository.java
@@ -1,6 +1,7 @@
 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 com.github.nkiri.core.domain.model.user.UserRepository;
 import org.springframework.stereotype.Repository;
 
@@ -13,7 +14,7 @@ public class DefaultUserRepository implements UserRepository {
     this.userMapper = userMapper;
   }
 
-  public User get(String userId) {
+  public User get(UserId userId) {
     return userMapper.get(userId);
   }
 }

UserMapper

index 1566960..d604e3a 100644
--- 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
@@ -1,9 +1,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;
 
 @Mapper
 public interface UserMapper {
-  User get(String userId);
+  User get(UserId userId);
 }

実行結果(エラー)

  • パラメータで渡しているUserIdクラスに、userIdフィールドのgetterメソッドがないと言われている
    • UserMapper.xml内で #{userId}としていると自動でUserId.userIdを参照しようとしてくれる模様
    • しかし今回はUserId.idを参照してほしいのでこれだとダメ
Exception in thread "main" org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'userId' in 'class com.github.nkiri.core.domain.model.user.UserId'

修正

方法1. 参照するフィールド名の変更

  • idフィールドを参照するようにしてあげれば良いだけ
index 31b127f..0964be1 100644
--- 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
@@ -9,7 +9,7 @@
     , FAMILY_NAME as familyName
     FROM USERS
     <where>
-      USER_ID = #{userId}
+      USER_ID = #{id}
     </where>
     ;
   </select>

方法2. @Paramを使用する(引数が複数の場合に使用する)

  • 引数が複数ある場合は@Paramを使用してxml側で引数の値を参照できるようにする
    • 今回は引数1つなので普通はこの方法は取らないらしいが、一応これでも動く
index d604e3a..1154437 100644
--- 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,9 @@ 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);
+  User get(@Param("userId") UserId userId);
 }
index 31b127f..6fe3a2d 100644
--- 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
@@ -9,7 +9,7 @@
     , FAMILY_NAME as familyName
     FROM USERS
     <where>
-      USER_ID = #{userId}
+      USER_ID = #{userId.id}
     </where>
     ;
   </select>

実行結果

User {userId: hogefuga, firstName: ほげ, familyName: ふが}

まとめ

  • resultMapを使うことによって、DB設計がイケてなくてもJavaクラスの設計に影響を与えずにいい感じにマッピングすることができそう
  • associationやcollectionの使い方についてはまた今度調べてみる