Spring Bootキャンプシリーズ、Spring Boot + MyBatis編です。
今回の目的
Spring BootアプリでMyBatisを利用してDBアクセスを行います。
今回使用するライブラリ
- spring-boot-starter:2.2.0.M4
- mybatis-spring-boot-starter:2.0.1
- mybatis-spring-boot-starter-test:2.0.1
- h2:1.4.199
- lombok:1.18.8
以降の手順のいくつかはSpring Initializrでプロジェクトを作成することにより解決されます。
DBアクセス
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
);
mybatis-spring-boot-starter
Spring BootでMyBatisを利用するためのスターターです。
ぶっちゃけ詳細は開発者の@kazuki43zooさんの記事が詳しいので、そちらを見たほうが良いですw
Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでMyBatisを使用するためのBean定義を自動的に行ってくれます。開発者は依存関係にmybatis-spring-boot-starterを追加するだけでOKです。
pom.xml
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
src/main/java/*/Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
メインクラスはSpring Bootアプリのデフォルトから何も変更していません。
Domainクラス
DBのデータをマッピングするドメインクラスは、普通のJava BeanでOKです。
以下では、GetterやSetterの実装を省略するため、Lombokの@Data
を利用しています。
src/main/java/*/domain/Todo.java
@Data
public class Todo {
private String todoId;
private String todoTitle;
private boolean finished;
private LocalDateTime createdAt;
}
Repositoryインターフェイス
SpringのRepositoryインターフェイスには、MyBatisのMapperを対応させます。
MyBatisのMapperは以下のいずれかの方法で実装することができます。
- Repositoryインターフェイスに対応するMapper XMLを作成する
- Repositoryインターフェイスに
@Mapper
を付与する
使いたいほうを使ってください!w
今回はアノテーションベースSpring Bootに合わせてMyBatisのMapperも@Mapper
で実装します。
src/main/java/*/repository/TodoRepository.java
@Mapper // (1)
public interface TodoRepository {
// (2)
@Select("SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #{todoId}")
Optional<Todo> findById(String todoId);
@Select("SELECT todo_id, todo_title, finished, created_at FROM todo")
Collection<Todo> findAll();
@Insert("INSERT INTO todo (todo_title, finished, created_at) VALUES (#{todoTitle}, #{finished}, #{createdAt})")
@Options(useGeneratedKeys = true, keyProperty = "todoId") // (3)
void create(Todo todo);
@Update("UPDATE todo SET todo_title = #{todoTitle}, finished = #{finished}, created_at = #{createdAt} WHERE todo_id = #{todoId}")
boolean updateById(Todo todo);
@Delete("DELETE FROM todo WHERE todo_id = #{todoId}")
void deleteById(Todo todo);
@Select("SELECT COUNT(*) FROM todo WHERE finished = #{finished}")
long countByFinished(boolean finished);
}
(1) Repositoryインターフェイスに@Mapper
を付与すると、MyBatisが自動的にスキャンしてMapperに登録してくれます。Repositoryインターフェイスはメインクラス配下のパッケージに置きましょう。
(2) メソッドに付与した@Select
・@Insert
・@Update
・@Delete
に、実行するSQLを実装します。SQL内の#{}
で引数を利用していますが、同じファイル内に記載するのでXMLより分かりやすいですね。
(3) @Options
は通常と異なる設定でSQLを実行する必要がある場合に付与します。ここではテーブルのキー項目がIDENTITY列のためDB側で自動採番されますが、@Options
を利用することで自動採番されたIDを利用することができます。
複数行のSQL
上ではSQLを1行で記載していますが、可読性が悪いので複数行で記載することをお勧めします。
こんな感じで文字列結合する形になるので、XMLよりちょっと見にくいですね。
@Select("SELECT"
+ " todo_id,"
+ " todo_title,"
+ " finished,"
+ " created_at"
+ " FROM todo"
+ " WHERE"
+ " todo_id = #{todoId}")
Optional<Todo> findById(String todoId);
Select結果の自動マッピングと手動マッピング
例示したSelect文の結果カラム名とTodoクラスのプロパティ名が異なるため、Select結果はマッピングに失敗します。(todo_title
とtodoTitle
など)
これを解決するには以下のいずれかの方法があります。
- MyBatisのネーミングルールによる自動マッピング
-
@Results
および@ResultMap
による手動マッピング
個人的には、自動マッピングが利用できるようにプロパティ名を命名し、ルールから逸脱する場合のみ手動マッピングすることをお勧めします。
自動マッピング
アンダースコア区切りのカラム名とキャメルケースのプロパティ名が一致していれば、MyBatisのネーミングルールによる自動マッピングが可能です。
Spring Bootの設定ファイルに以下の設定を追加します。
src/main/resources/application.yml
mybatis:
configuration:
map-underscore-to-camel-case: true
Note.
mybatis.configuration.*
プロパティでMyBatisの設定を変更することができます。設定可能なプロパティはMyBatis 3 - 設定を見てください。
手動マッピング
カラム名とプロパティ名が一致しない場合、SQLごとに@Results
および@ResultMap
による手動マッピングを定義する必要があります。
-
@Results
で手動マッピングルールを定義する -
@Results
で定義したルールを流用したければ、@ResultMap
で@Results
のIDを指定する
@Select("SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #{todoId}")
@Results(id = "todo", value = {
@Result(column = "todo_id", property = "todoId"),
@Result(column = "todo_title", property = "todoTitle"),
@Result(column = "finished", property = "finished"),
@Result(column = "created_at", property = "createdAt") })
Optional<Todo> findById(String todoId);
@Select("SELECT todo_id, todo_title, finished, created_at FROM todo")
@ResultMap("todo")
Collection<Todo> findAll();
動的SQLと共通化
Mapper XMLで可能だった動的SQL(<if>
)や共通化(<sql>
)は、SqlProviderで実現します。
// (2)
@SelectProvider(type = TodoSqlProvider.class, method = "find")
Optional<Todo> findById(String todoId);
@SelectProvider(type = TodoSqlProvider.class, method = "find")
Collection<Todo> findAll();
// (1)
public class TodoSqlProvider {
public String find(String todoId) {
return new SQL() {{
SELECT("todo_id", "todo_title", "finished", "created_at");
FROM("todo");
if (todoId != null) {
WHERE("todo_id = #{todoId}");
}
}}.toString();
}
}
(1) SqlProviderクラスを実装して、メソッドでnew SQL()
を利用してSQLを組み立てます。例ではインスタンスイニシャライザ(new SQL() {{ココ}}
)を利用していますが、もちろん普通にメソッドチェーンで書いても良いです。
Note.
WHERE句には引数を直接埋め込むこともできますが、必ず#{}
を利用しましょう。
#{}
はSQLインジェクションを防止してくれます。
(2) メソッドに@Select
の代わりに@SelectProvider
を付与して、実装したSqlProviderクラスとメソッドを指定します。
Note.
インターフェイスのdefaultメソッドはSqlProviderメソッドにできません。必ずクラスを作る必要があります。
これはSqlProviderメソッドを利用する際、SqlProviderクラスのインスタンスが生成されるためです。
mybatis-spring-boot-starter-test
Spring BootでMyBatisをテストするためのスターターです。
Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでMyBatisのMapperをテストするためのBean定義を自動的に行ってくれます。開発者は依存関係にmybatis-spring-boot-starter-testを追加して、少しの設定をするだけでOKです。
pom.xml
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
JUnitテストケース
JUnitテストケースでは、クラスに@MyBatisTest
を付与してテスト対象のリポジトリを@Autowired
するだけでOKです。
src/test/java/*/repository/TodoRepositoryTest.java
@MybatisTest
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
Collection<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()));
}
@MyBatisTest
はMyBatisを利用するためのBean定義、DBにアクセスするDataSource
等のBean定義を自動的に行ってくれますが、さらに**@Transactional
を付与してくれます。**
@Transactional
により、@Sql
やテスト内で実行したSQLはテスト終了後にロールバックされます。インメモリではない実際のDBにアクセスしてテストするときもテストの独立性が保たれるので、安心ですね。
まとめ
MyBatisのスターターを利用することで、設定周りを省略してMapperの実装に注力することができました。設定の変更もSpring Bootの設定ファイルにプロパティを定義するだけなので、非常に楽ですね。
MapperをXMLで実装する場合はもう少し設定する必要がありそうですが、それはまたの機会に。