🐧

MyBatisでテキストブロック(Text Blocks)とレコード(Records)を使う

2022/07/03に公開

Java 15 でテキストブロック、Java 16 でレコードが入り、先日リリースされた MyBatis 3.5.10 ではレコードに対応しています。

MyBatisでテキストブロック&レコードを使う準備が整ったので、実際利用してみます。

今回試したコードは下記のプロジェクトにあります。

環境情報

  • Java 17
  • Spring Boot 2.6.8
  • DB: H2 Database

下記のようなテーブルを利用しました。

CREATE TABLE customers (
	id INT PRIMARY KEY AUTO_INCREMENT,
	first_name VARCHAR(30) NOT NULL,
	last_name VARCHAR(30) NOT NULL,
	address VARCHAR(100)
);

利用方法

MyBatis 3.5.10 の利用

MyBatisの依存を明示的に指定しているならば、そこで指定しているバージョンを3.5.10に変更するだけとなりますが、Spring Bootでmybatis-spring-boot-starterを利用している場合、現在mybatis-spring-boot-starterで最新の2.2.2では、MyBatis本体は3.5.9になるので、追加でMyBatis自体の指定が必要になります。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.flywaydb:flyway-core'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
+   implementation 'org.mybatis:mybatis:3.5.10'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

エンティティ

エンティティクラスをレコードで定義します。

package com.example.domain;

public record Customer(Integer id, String firstName, String lastName, String address) {
}

Mapper

MapperでアノテーションでSQLを書きますが、ここでテキストブロックを利用します。

ただ、レコードにすることでuseGeneratedKeysが利用できなくなるので、AUTO_INCREMENTで払い出したIDを取得するために、H2 DatabaseのSCOPE_IDENTITYを利用してレコードを取得するといっためんどくさいコードになってしまいました。詳しくは後述します。

package com.example.repository;

import java.util.List;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

import com.example.domain.Customer;

@Repository
@Mapper
public interface CustomerRepository {

    @Select("""
SELECT
  *
FROM
  customers
ORDER BY
  id
    """)
    public List<Customer> findAll();

    @Select("""
SELECT
  *
FROM
  customers
WHERE
  id = #{id}
    """)
    public Customer findOne(@Param("id") int id);

    @Insert("""
INSERT INTO customers(
  first_name
  , last_name
  , address
)
VALUES(
  #{firstName}
  , #{lastName}
  , #{address}
)
""")
    public void insert(Customer customer);

    @Select("""
SELECT
  *
FROM
  customers
WHERE
  id = SCOPE_IDENTITY()
    """)
    public Customer findLast();

    @Update("""
UPDATE customers
  SET
    first_name = #{customer.firstName}
    , last_name = #{customer.lastName}
    , address = #{customer.address}
WHERE
  id = #{id}
""")
    public void update(@Param("id") int id, @Param("customer") Customer customer);

    @Delete("""
DELETE FROM customers
WHERE
  id = #{id}
    """)
    public void delete(@Param("id") Integer id);

    @Select("""
SELECT
  *
FROM
  customers
WHERE
  first_name LIKE '%' || #{firstName} || '%'
ORDER BY
  id
    """)
    public List<Customer> findByFirstName(@Param("firstName") String firstName);

    @Delete("""
DELETE FROM customers
    """)
    public void deleteAll();
}

実際に書いてみて

テキストブロック

テキストブロックのおかげでSQLがアノテーションで書きやすくなりました。
複数行書くために今までMapperはGroovyで書いてたりしたのですが、これでJavaで書けるようになりました。

ちなみにSQLを別ファイルで書くといった方法もありますが、(個人的には)コードとSQLは近くにあった方がわかりやすいと思っているので、これはとても助かります。

レコード

レコードをエンティティとして使うのはちょっと微妙かなと思っています。

レコードはイミュータブルなので、useGeneratedKeysによる生成したIDの設定が利用できません。
useGeneratedKeysは、引数で受け取ったオブジェクトに対して値を設定するためです。レコードに対してuseGeneratedKeysを利用すると設定できずにエラーとなります。
今回は別途SQLを発行してINSERTしたレコードを取り出すといったとてもめんどくさいものになってしまいました。

ちなみに、PostgreSQLのようにRETURNINGがあるDBならば、RETURNING使ってレコード返すことで回避できます。(普段PostgreSQL使っているのもあって、useGeneratedKeysは使わずにRETURNING派です)

    @Select("""
INSERT INTO customers(
  first_name
  , last_name
  , address
)
VALUES(
  #{firstName}
  , #{lastName}
  , #{address}
)
RETURNING *
""")
    public Customer insert(Customer customer);

あと、レコードはフィールドが多くなると、コンストラクタの引数が多くなってしんどいので、エンティティ的なものとして利用するにはBuilder的なものも必要になりそうです。

イミュータブルだと困るシチュエーションもちょこちょこありそうで、今回の件とは関係なく、個人的にはレコードはそこまで積極的に使わないかもなぁ、、と思っています。(フィールド少ないもの、タプル的に使うような場合くらい?)

Discussion