2018年最初のエントリーは・・・@sndr さんの「Spring Data JDBC Preview」を見て「へ~」と思ったSpring Data JDBCを試した際のメモにしました。まだ単純なCRUDレベルのサポートだけのようですが、Spring Data JDBCが正式にリリースされてSpring Data RESTのサポート対象になることを(非常に)期待しています!
検証バージョン
- Spring Data JDBC 1.0.0.BUILD-SNAPSHOT(2018-01-08時点)
- Spring Boot 2.0.0.M7
- MyBatis Spring Boot Starter 1.3.1 (MyBatis 3.4.5 + MyBatis Spring 1.3.1)
- H2 Database 1.4.196
デモプロジェクト
本エントリーで記載したソースは、以下のリポジトリで公開しています。(Spring JDBCとMyBatisを混在させて検証を行っている関係で、エントリー内の記載と異なる箇所があります)
開発プロジェクトの作成
まず、SPRING INITIALIZRにて、Dependenciesに「MyBatis」「H2」を選択してプロジェクトを作成する。(本エントリではMavenプロジェクト前提での説明になります)
次に、SPRING INITIALIZRで作成したプロジェクトに「spring-data-jdbc」を追加する。
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
DBのセットアップ
テーブルを作るためのDDLを用意する。
CREATE TABLE IF NOT EXISTS todo (
id IDENTITY
,title TEXT NOT NULL
,details TEXT
,finished BOOLEAN NOT NULL
);
ドメインオブジェクトの作成
TODOテーブルのレコードを表現するTodoオブジェクトを作る。キー値を保持するプロパティに@Id
を付与する。
package com.example.demo.domain;
import org.springframework.data.annotation.Id;
public class Todo {
@Id
private int id;
private String title;
private String details;
private boolean finished;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public boolean isFinished() {
return finished;
}
public void setFinished(boolean finished) {
this.finished = finished;
}
}
Repositoryの作成
ドメインオブジェクトを操作するためのRepositoryインタフェースを作成する。Spring Dataが提供するCroudRepository
を継承するのがポイント。
package com.example.demo.repository;
import org.springframework.data.repository.CrudRepository;
import com.example.demo.domain.Todo;
public interface TodoRepository extends CrudRepository<Todo, Integer> {
}
こうすることで・・・CrudRepository
に定義されている以下のメソッドを使用してTodoオブジェクトを操作することができる。
package org.springframework.data.repository;
import java.util.Optional;
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
SQLの実行方法
Spring Data JDBCは、SQLを実行する方法を抽象化するためのインタフェースとしてDataAccessStrategy
を用意しており、現時点では「Spring JDBC(NamedParameterJdbcOperations
)実装」と「MyBatis実装」が内蔵されている。
Spring JDBC実装の利用
Spring JDBC実装を利用すると、CrudRepository
に定義されているメソッドを呼び出した時に実行するSQLは自動生成される(=CRUD操作についてはSQLを書く必要がない)。
Bean定義例
@EnableJdbcRepositories
を付与したコンフィギュレーションクラスを作成し、DataAccessStrategy
としてDefaultDataAccessStrategy
(Spring JDBC実装)のBeanを定義する。デフォルトでサポートしていない型変換が必要になる場合は、ConversionCustomizer
のBeanを定義すればよい。本エントリーでは、H2 DatabaseのTEXT型(Clob
)をString
に変換するためのConverter
を追加している。ちなみに・・・TEXTの代わりにVARCHARを使えばConversionCustomizer
のBean定義は省略できる。
@EnableJdbcRepositories
@Configuration
public class SpringDataJdbcConfig {
@Bean
DataAccessStrategy dataAccessStrategy(NamedParameterJdbcOperations namedParameterJdbcOperations,
JdbcMappingContext context) {
return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), namedParameterJdbcOperations,
context);
}
@Bean
ConversionCustomizer conversionCustomizer() {
return conversionService -> {
// for converter 'TEXT' column
conversionService.addConverter(Clob.class, String.class, clob -> {
try {
return clob == null ? null : clob.getSubString(1L, (int) clob.length());
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
};
}
}
Note:
本エントリでは詳細に触れないが、カラム名とプロパティ名のネーミング戦略を決めるためのインタフェースとして
NamingStrategy
が用意されている。デフォルトではDefaultNamingStrategy
という実装クラスが使用され、デフォルト動作を変更したい場合はNamingStrategy
のBeanを定義しておくと自動で検出してくれる。
MyBatis実装の利用
MyBatis実装を利用する場合は、CrudRepository
に定義されているメソッドを呼び出した時に実行するSQLをMyBatis側に定義しておく必要がある。(=CRUD操作についてもSQLを書く必要がある)。
Bean定義例
@EnableJdbcRepositories
を付与したコンフィギュレーションクラスを作成し、DataAccessStrategy
としてMyBatisDataAccessStrategy
(MyBatis実装)のBeanを定義する。
@EnableJdbcRepositories
@Configuration
public class SpringDataJdbcConfig {
@Bean
DataAccessStrategy dataAccessStrategy(SqlSessionFactory sqlSessionFactory) {
return new MyBatisDataAccessStrategy(sqlSessionFactory);
}
}
MyBatisの設定例
Mapper XMLファイルのロケーションやタイプエイリアスの設定を行う。
mybatis.mapper-locations=classpath:/com/example/demo/mapper/*Mapper.xml
mybatis.type-aliases-package=com.example.demo.domain
SQLの定義例
CrudRepository
のメソッドに対応するSQL定義を行う。Spring Data JDBC経由でMyBatisを利用する場合は、いくつかの特殊ルールを意識してSQL定義を行う必要がある。
- 対応するSQL定義のネームスペースは「ドメインオブジェクトのFQCN + "Mapper"」にする
- パラメータは
MyBatisContext
というクラスにラップされて渡される(MyBatisContext
の抜粋は別途掲載) -
CrudRepository
のメソッドとSQL定義の対応が1対1というわけではない(MyBatisのMapperインタフェースの対応ルールと異なる)。具体的な対応は、Spring Data JDBCのREADMEを参照。
<?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.example.demo.domain.TodoMapper">
<!-- statements for CrudRepository method -->
<insert id="insert" useGeneratedKeys="true" keyProperty="instance.id">
INSERT INTO todo
(title, details, finished)
VALUES
(#{instance.title}, #{instance.details}, #{instance.finished})
</insert>
<update id="update">
UPDATE todo SET
title = #{instance.title}, details = #{instance.details}, finished = #{instance.finished}
WHERE
id = #{instance.id}
</update>
<delete id="delete">
DELETE FROM todo WHERE id = #{id}
</delete>
<delete id="deleteAll">
DELETE FROM todo
</delete>
<select id="existsById" resultType="_boolean">
SELECT count(*) FROM todo WHERE id = #{id}
</select>
<select id="findById" resultType="Todo">
SELECT
id, title, details, finished
FROM
todo
WHERE
id = #{id}
</select>
<select id="findAll" resultType="Todo">
SELECT
id, title, details, finished
FROM
todo
ORDER BY
id
</select>
<select id="findAllById" resultType="Todo">
SELECT
id, title, details, finished
FROM
todo
<where>
<foreach collection="id" item="idValue" open="id in("
separator="," close=")">
#{idValue}
</foreach>
</where>
ORDER BY
id
</select>
<select id="count" resultType="_long">
SELECT count(*) FROM todo
</select>
</mapper>
package org.springframework.data.jdbc.mybatis;
import java.util.Map;
public class MyBatisContext {
private final Object id;
private final Object instance;
private final Class domainType;
private final Map<String, Object> additonalValues;
public MyBatisContext(Object id, Object instance, Class domainType, Map<String, Object> additonalValues) {
this.id = id;
this.instance = instance;
this.domainType = domainType;
this.additonalValues = additonalValues;
}
public Object getId() {
return id;
}
public Object getInstance() {
return instance;
}
public Class getDomainType() {
return domainType;
}
public Object get(String key) {
return additonalValues.get(key);
}
}
実装の併用
本エントリーでは説明しません+検証もしていませんが、CascadingDataAccessStrategy
を使用して複数の実装(例、Spring JDBCとMyBatis)を併用することもできるみたいです。
Repositoryの利用
Spring Data JDBCのRepositoryは、他のSpring Dataプロジェクトと同様にインジェクションして使用する。
@Autowired
private TodoRepository todoRepository;
@Test
public void insertAndFineById() {
Todo newTodo = new Todo();
newTodo.setTitle("飲み会");
newTodo.setDetails("銀座 19:00");
todoRepository.save(newTodo);
Optional<Todo> todo = todoRepository.findById(newTodo.getId());
Assertions.assertThat(todo.isPresent()).isTrue();
Assertions.assertThat(todo.get().getId()).isEqualTo(newTodo.getId());
Assertions.assertThat(todo.get().getTitle()).isEqualTo(newTodo.getTitle());
Assertions.assertThat(todo.get().getDetails()).isEqualTo(newTodo.getDetails());
Assertions.assertThat(todo.get().isFinished()).isFalse();
}
カスタム操作の追加
Spring Dataには「カスタム操作(カスタムメソッド)を追加する仕組み」があり、この仕組みはSpring Data JDBCでも利用することができる。
カスタムインタフェースの作成
カスタム操作(カスタムメソッド)を定義するためのインタフェースを定義する。
package com.example.demo.repository;
import com.example.demo.domain.Todo;
public interface CustomizedTodoRepository {
Iterable<Todo> findAllByFinished(boolean finished);
}
作成したインタフェースをTodoRepository
で継承する。
package com.example.demo.repository;
import org.springframework.data.repository.CrudRepository;
import com.example.demo.domain.Todo;
public interface TodoRepository extends CrudRepository<Todo, Integer>, CustomizedTodoRepository {
}
Spring JDBC実装の作成
Spring JDBCを利用する場合は、以下のような実装クラスを作成する。
package com.example.demo.repository;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import com.example.demo.domain.Todo;
public class CustomizedTodoRepositoryImpl implements CustomizedTodoRepository {
private static final RowMapper<Todo> ROW_MAPPER = new BeanPropertyRowMapper<>(Todo.class);
private final NamedParameterJdbcOperations namedParameterJdbcOperations;
public CustomizedTodoRepositorySpringJdbcImpl(NamedParameterJdbcOperations namedParameterJdbcOperations) {
this.namedParameterJdbcOperations = namedParameterJdbcOperations;
}
public Iterable<Todo> findAllByFinished(boolean finished) {
return this.namedParameterJdbcOperations.query(
"SELECT id, title, details, finished FROM todo WHERE finished = :finished ORDER BY id",
new MapSqlParameterSource("finished", finished), ROW_MAPPER);
}
}
MyBatis実装の作成
MyBatisを利用する場合は、以下のような実装クラスを作成する。
package com.example.demo.repository;
import org.apache.ibatis.session.SqlSession;
import com.example.demo.domain.Todo;
public class CustomizedTodoRepositoryImpl implements CustomizedTodoRepository {
private final String NAMESPACE = Todo.class.getName() + "Mapper";
private final SqlSession sqlSession;
public CustomizedTodoRepositoryMyBatisImpl(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
public Iterable<Todo> findAllByFinished(boolean finished) {
return this.sqlSession.selectList(NAMESPACE + ".findAllByFinished", finished);
}
}
SQL定義も追加する。
<?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.example.demo.domain.TodoMapper">
<!-- ... -->
<!-- statements for custom repository method -->
<select id="findAllByFinished" resultType="Todo">
SELECT
id, title, details, finished
FROM
todo
WHERE
finished = #{finished}
ORDER BY
id
</select>
</mapper>
Spring Boot連携
一応・・・開発者の方の個人リポジトリにspring-data-jdbc-boot-starterがあるようですが、現時点ではどこにもデプロイされていないのでローカルリポジトリにインストールして使う必要があります。(とりあえず私は今回は使いませんでした)
まとめ
まだまだ開発途中で成長するライブラリだと思うので、動向を見守っていきたいと思います。Repositoryインタフェースにカスタムメソッド定義できてアノテーションでSQLを指定できるようになるとかなり実用的になりそうな気がします(READMEを見る限りだとサポートする計画はありそう)。
そして、Spring Data RESTとの連携がサポートされれば最高だ~。