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」を追加する。

pom.xml
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jdbc</artifactId>
    <version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

DBのセットアップ

テーブルを作るためのDDLを用意する。

src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS todo (
    id IDENTITY
    ,title TEXT NOT NULL
    ,details TEXT
    ,finished BOOLEAN NOT NULL
);

ドメインオブジェクトの作成

TODOテーブルのレコードを表現するTodoオブジェクトを作る。キー値を保持するプロパティに@Idを付与する。

src/main/java/com/example/demo/domain/Todo.java
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を継承するのがポイント。

src/main/java/com/example/demo/repository/TodoRepository.java
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オブジェクトを操作することができる。

参考:CrudRepositoryの抜粋
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ファイルのロケーションやタイプエイリアスの設定を行う。

src/main/resources/application.properties
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を参照。
src/main/resources/com/example/demo/mapper/TodoMapper.xml
<?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>
参考:MyBatisContextの抜粋
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でも利用することができる。

カスタムインタフェースの作成

カスタム操作(カスタムメソッド)を定義するためのインタフェースを定義する。

src/main/java/com/example/demo/repository/CustomizedTodoRepository.java
package com.example.demo.repository;

import com.example.demo.domain.Todo;

public interface CustomizedTodoRepository {

    Iterable<Todo> findAllByFinished(boolean finished);

}

作成したインタフェースをTodoRepositoryで継承する。

src/main/java/com/example/demo/repository/TodoRepository.java
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を利用する場合は、以下のような実装クラスを作成する。

src/main/java/com/example/demo/repository/CustomizedTodoRepositoryImpl.java
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を利用する場合は、以下のような実装クラスを作成する。

src/main/java/com/example/demo/repository/CustomizedTodoRepositoryImpl.java
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定義も追加する。

src/main/resources/com/example/demo/mapper/TodoMapper.xml
<?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との連携がサポートされれば最高だ~。