Spring Bootキャンプシリーズ、Spring Boot + Spring Data JDBC + MyBatis編です。
今回の目的
Spring BootアプリでSpring Data JDBCのMyBatis連携機能を利用してDBアクセスを行います。
- Spring Data JDBCを利用したDBアクセスはこちら
- MyBatisを利用したDBアクセスはこちら
今回使用するライブラリ
- spring-boot-starter:2.2.0.M4
- spring-boot-starter-data-jdbc:2.2.0.M4
- mybatis-spring-boot-starter:2.0.1
- spring-boot-starter-test:2.2.0.M4
- mybatis-spring-boot-starter-test:2.0.1
- h2:1.4.199
- lombok:1.18.8
以降の手順のいくつかはSpring Initializrでプロジェクトを作成することにより解決されます。
DBアクセス
Spring Data JDBC + MyBatisの実装を行う前に、まずはDBアクセスするための設定を行います。
DBアクセスの定義
Spring BootではDBにアクセスするDataSource
等のBean定義を自動的に行ってくれます。
デフォルトではインメモリのH2データベースにアクセスするため、依存関係にh2を追加する必要があります。
pom.xml
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Note.
Spring Bootの設定ファイルでドライバと接続先を変更すれば、別DBにアクセスできます。(依存関係に対応するドライバを追加する必要があります)src/main/resources/application.yml
spring: datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/testdb username: postgres password: postgres
src/main/resources直下に以下のSQLファイルを作成すると、アプリ起動時にデータベースの初期化も自動的に行うこともできます。
デフォルトで認識されるSQLファイルは以下の通りです。
- schema.sql
- schema-${platform}.sql
- data.sql
- data-${platform}.sql
見たままですが、schema.sqlにはDDLを定義し、data.sqlにはDMLを定義します。
Note.
${platform}
はspring.datasource.platform
プロパティで指定します。
単体テスト時はH2、結合テスト時はPostgresqlといった使い分けができそうですね。
今回はschema.sqlを作成してテーブルを作成します。
src/main/resources/schema.sql
create table if not exists todo (
todo_id identity,
todo_title varchar(30),
finished boolean,
created_at timestamp
);
spring-boot-starter-data-jdbc + mybatis-spring-boot-starter
Spring BootでSpring Data JDBCとMyBatisを利用するためのスターターです。
Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでSpring Data JDBCとMyBatisを使用するためのBean定義を自動的に行ってくれます。
Spring Data JDBCとMyBatisを連携するには、依存関係にこれらを追加して、連携する@Configuration
クラスを実装すればOKです。
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
src/main/java/*/SpringDataJdbc.java
@Configuration
public class SpringDataJdbcConfig {
@Bean
@Primary
public DataAccessStrategy mybatisDataAccessStrategy(SqlSession sqlSession) {
return new MyBatisDataAccessStrategy(sqlSession);
}
}
連携のため、Spring Data JDBCのDataAccessStrategyにMyBatisDataAccessStrategyクラスを利用します。
DataAccessStrategyはBean定義すれば、自動的に適用されます。
Note.
自動的にDefalutDataAccessStrategyクラスのBeanが生成されるため、DataAccessStrategyのBean定義には@Primary
を付与しないとBeanが重複してエラーとなります。
src/main/java/*/Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
メインクラスはSpring Bootアプリのデフォルトから何も変更していません。
実装した@Configuration
クラスはメインクラス配下のパッケージにあれば、自動的にスキャンされます。
Spring Data JDBC + MyBatisでできること
CrudRepositoryのメソッドを実行したとき、Spring DataのNamedParameterJdbcTemplateではなくMyBatisを利用してSQLを発行することができます。
CrudRepositoryから連携されるMyBatisのMapperにはネーミングルールがあります。
[DomainクラスのFQCN]Mapper.[予約メソッド名]
com.example.domain.Todo
クラスなら、Mapper名はcom.example.domain.TodoMapper
のようになります。
予約メソッド名は例えば以下のようになりますが、詳細はSpring Data JDBC - MyBatis Integrationを確認してください。
- Mapperの
findById
-> CrudRepositoryのfindById
メソッドで使用される - Mapperの
insert
-> CrudRepositoryのsave
やsaveAll
メソッドで使用される
ここで注意すべきことは、MyBatisで自由にカスタムクエリを作れないということです。
カスタムクエリを作りたければ、Spring Data JDBCで@Query
を利用するか、大人しくMyBatisを直接使いましょう。
また、CrudRepositoryメソッドの引数やMapperの戻り値はMyBatisContext
で連携されます。
このため、MyBatisのみを使う場合とはMapperの実装方法が微妙に異なります。
Domainクラス
DBのデータをマッピングするドメインクラスには、プライマリキーを識別する@ID
を付与します。
以下では、GetterやSetterの実装を省略するため、Lombokの@Data
を利用しています。
src/main/java/*/domain/Todo.java
@Data
public class Todo {
@Id
private String todoId;
private String todoTitle;
private boolean finished;
private LocalDateTime createdAt;
}
Note.
テーブル名とDomainクラス名が異なるときは、クラスに@Table("テーブル名")
を付与すればOKです。
Repositoryインターフェイス
SpringのRepositoryインターフェイスは、Spring DataのCrudRepositoryインターフェイスを継承します。
src/main/java/*/repository/TodoRepository.java
// (1)
public interface TodoRepository extends CrudRepository<Todo, String> {
// (2)
}
(1) RepositoryインターフェイスはCrudRepositoryを継承します。ジェネリクスは<Domainクラスの型, IDの型>
です。
(2) CrudRepositoryを継承すると、自動的にCRUDをサポートするデフォルトメソッド(クエリ)が利用可能になります。提供されるメソッドはCrudRepositoryのJavaDocを確認すると分かります。
MyBatisのMapperインターフェイス
MyBatisのMapperインターフェイスは、Domainクラスと同じパッケージに実装します。
MapperはXMLと@Mapper
を付与したインターフェイスのいずれかで実装できますが、ここではインターフェイスで実装することにします。
src/main/java/*/domain/TodoMapper.java
// (1)
@Mapper
public interface TodoMapper {
// (2)
@Select("SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #{id}")
Optional<Todo> findById(MyBatisContext context); // (3)
@Select("SELECT todo_id, todo_title, finished, created_at FROM todo")
Collection<Todo> findAll();
@Insert("INSERT INTO todo (todo_title, finished, created_at) VALUES (#{instance.todoTitle}, #{instance.finished}, #{instance.createdAt})")
@Options(useGeneratedKeys = true, keyProperty = "instance.todoId")
Todo insert(MyBatisContext context);
@Update("UPDATE todo SET todo_title = #{instance.todoTitle}, finished = #{instance.finished}, created_at = #{instance.createdAt} WHERE todo_id = #{id}")
Todo update(MyBatisContext context);
@Delete("DELETE FROM todo WHERE todo_id = #{id}")
void delete(MyBatisContext context);
@Select("SELECT COUNT(*) FROM todo WHERE finished = 'FALSE'")
long count(MyBatisContext context);
}
(1) Mapperインターフェイスに@Mapper
を付与すると、MyBatisが自動的にスキャンしてMapperに登録してくれます。Mapperインターフェイスはメインクラス配下のパッケージに置きましょう。
(2) メソッドに付与した@Select
・@Insert
・@Update
・@Delete
に、実行するSQLを実装します。SQL内の#{}
で引数を利用していますが、以下のルールに従う必要があります。
- Domainクラスの
@ID
項目 ->#{id}
- Domainクラスの
@ID
以外の項目 ->#{instance.[プロパティ名]}
(3) メソッドシグネチャには以下のルールがあります。
- メソッド名は先述の予約メソッド名にする必要があります。メソッド名が異なると呼び出されません。
- メソッド引数は必ず
MyBatisContext
にする必要があります。 - メソッド戻り値はCrudRepositoryメソッドと合わせる必要がありますが、いくつか異なります。
-
Optional
はあってもなくてもOKでした。 -
Iterable
は対応しておらず、Collection
等にする必要がありました。
-
Note.
ここではテーブルのカラム名とDomainクラスのプロパティ名の違いを吸収するため、Spring Bootの設定ファイルにmybatis.configuration.map-underscore-to-camel-case
プロパティを指定しています。
DomainクラスとMapperインターフェイスのパッケージを分離
前述ではDomainクラスとMapperインターフェイスを同パッケージに置きましたが、数が増えると整理が面倒なので、整理のためにパッケージを分離します。
CrudRepositoryと連携するMapperのネーミングルールはNamespeceStrategyインターフェイスを実装することで変更することが可能です。
が、、、NamespeceStrategyで変更可能なのは[DomainクラスのFQCN]Mapper
の部分だけで、予約メソッド名を変更するにはMyBatisDataAccessStrategyを拡張する必要があります。
ここでは、NamespeceStrategyを実装してネーミングルールを以下のように変更します。
[Domainクラスのパッケージ名].mapper.[Domainクラス名]Mapper.[予約メソッド名]
src/main/java/*/SpringDataJdbc.config
@Configuration
public class SpringDataJdbcConfig {
@Bean
@Primary
public DataAccessStrategy mybatisDataAccessStrategy(SqlSession sqlSession) {
MyBatisDataAccessStrategy strategy = new MyBatisDataAccessStrategy(sqlSession);
strategy.setNamespaceStrategy(new NamespaceStrategy() {
@Override
public String getNamespace(Class<?> domainType) {
return domainType.getPackage().getName() + ".mapper." + domainType.getSimpleName() + "Mapper";
}
});
return strategy;
}
}
NamespaceStrategyインターフェイスを実装してgetNamespace
メソッドでMapper名を決め、MyBatisDataAccessStrategyにセットします。
あとは、Mapperインターフェイスをmapper
パッケージに移動すればOKです。
spring-boot-starter-test + mybatis-spring-boot-starter-test
Spring BootとMyBatisをテストするためのスターターです。
Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでSpring Data JDBCとMyBatisを使用してテストするためのBean定義を自動的に行ってくれます。
Spring Data JDBCとMyBatisを連携したテストをするには、依存関係にこれらを追加して、少しの設定をするだけでOKです。
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>2.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
Note.
厳密にはmybatis-spring-boot-starter-testではなくmybatis-spring-boot-starterのみでもOKなんですが、ここではmainの文法とtestの文法を明確に分けるため使用します。
JUnitテストケース
JUnitテストケースでは、クラスに以下のアノテーションを付与してテスト対象のリポジトリを@Autowired
するだけでOKです。
-
@DataJdbcTest
-> Spring Data JDBCを有効化します。 -
@AutoConfigureMybatis
-> MyBatisを有効化します。 -
@Import
-> MyBatisDataAccessStrategyを有効化する設定を読み込みます。
src/test/java/*/repository/TodoRepositoryTest.java
@DataJdbcTest
@AutoConfigureMybatis // (1)
@Import(SpringDataJdbcConfig.class) // (2)
@Transactional // (3)
class TodoRepositoryTest {
@Autowired
TodoRepository todoRepository;
@Test
@Sql(statements = "INSERT INTO todo (todo_title, finished, created_at) VALUES ('sample todo', false, '2019-01-01')")
void testFindAll() {
// execute
Iterable<Todo> todos = todoRepository.findAll();
// assert
assertThat(todos)
.hasSize(1)
.extracting(Todo::getTodoTitle, Todo::isFinished, Todo::getCreatedAt)
.containsExactly(tuple("sample todo", false, LocalDate.of(2019, 1, 1).atStartOfDay()));
}
(1) @DataJdbcTest
と@MyBatisTest
を同時に使うことはできないので、@DataJdbcTest
に@AutoConfigureMybatis
を追加する形で使用します。
(2) @DataJdbcTest
等のテスト用Auto-Configは、軽量化のため限定的にBeanを読み込むため、独自の@Configuration
クラス等は読み込まれません。アプリ実行時とテスト時で異なる設定をする必要があるため、開発者はある程度Auto-Configを理解してないとダメですね。
(3) @DataJdbcTest
は@Transactional
を付与してくれないので、テスト終了後にロールバックするためには自分で@Transactional
を付与する必要があります。
Note.
Spring Boot 2.2.2から@DataJdbcTest
で@Transactional
が付与されるようになりました。
まとめ
Spring Data JDBCからMyBatisの連携も、ほとんど設定なしに実現することができました。
ただし、MyBatisの利用には一定の制限がつくことになるので、連携して何が嬉しいかと言われると、、、
DomainクラスへのCRUDに限定してMyBatisをシンプルに利用することを目的とするような設計思想であったり、DBの複雑なテーブルをまとめてViewとしてCRUDアクセスするような限定的なケースで利用するのかな、と思いました。