MyBatisでテキストブロック(Text Blocks)とレコード(Records)を使う
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