Spring Bootキャンプ：Spring Boot + Spring Data JDBC編

Spring Bootキャンプシリーズ、Spring Boot + Spring Data JDBC編です。

## 今回の目的

Spring BootアプリでSpring Data JDBCを利用してDBアクセスを行います。

## 今回使用するライブラリ

* spring-boot-starter:2.2.0.M4
* spring-boot-starter-data-jdbc:2.2.0.M4
* spring-boot-starter-test:2.2.0.M4
* h2:1.4.199
* lombok:1.18.8

以降の手順のいくつかはSpring Initializrでプロジェクトを作成することにより解決されます。

## DBアクセス

Spring Data JDBCの実装を行う前に、まずはDBアクセスするための設定を行います。

### DBアクセスの定義

Spring BootではDBにアクセスする`DataSource`等のBean定義を自動的に行ってくれます。
デフォルトではインメモリのH2データベースにアクセスするため、依存関係にh2を追加する必要があります。

**pom.xml**

```xml
    <dependencies>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
```

> Note.
> Spring Bootの設定ファイルでドライバと接続先を変更すれば、別DBにアクセスできます。（依存関係に対応するドライバを追加する必要があります）
> 
> **src/main/resources/application.yml**
> 
> ```yaml
> 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**

```sql
create table if not exists todo (
    todo_id identity,
    todo_title varchar(30),
    finished boolean,
    created_at timestamp
);
```

## spring-boot-starter-data-jdbc

Spring BootでSpring Data JDBCを利用するためのスターターです。
ぶっちゃけ詳細は開発者の[@kazuki43zooさんの記事](https://qiita.com/kazuki43zoo/items/01cec9197820af42f6b1)が詳しいので、そちらを見たほうが良いですw

Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリでSpring Data JDBCを使用するためのBean定義を自動的に行ってくれます。開発者は依存関係にspring-boot-starter-data-jdbcを追加するだけでOKです。

**pom.xml**

```xml
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
    </dependencies>
```

**src/main/java/*/Application.java**

```java
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

メインクラスはSpring Bootアプリのデフォルトから何も変更していません。

### そもそもSpring Data JDBCとは？

SpringのDBアクセスといえばSpring DataのJdbcTemplateかSpring Data JPAですが、Spring Data JDBCはその両方をハイブリッドしたようなものです。

Spring Data JPAのような文法でRepositoryを定義すると、JdbcTemplate（NamedParameterJdbcTemplate）を利用してSQLが実行されます。

Spring Data JDBCがサポートする主な機能は以下の通りです。

* CRUDを実現するシンプルなAPIを提供する
* `@Query`でカスタムクエリを定義できる
* SQL実行前後などのイベントをインターセプトする
* Spring Data JDBCを経由してMyBatisのSQLを実行できる -> 別エントリで！
* これらをAuto-Configする

Spring Data JPAと比べると、ぱっと見以下のような違いがあります。

* カスタムクエリはJPQLではなくSQL（NamedParameterJdbcTemplateの文法）で定義する
* Lazy Fetch（遅延読み込み）のような機能がない

### Domainクラス

DBのデータをマッピングするドメインクラスには、プライマリキーを識別する`@ID`を付与します。
以下では、GetterやSetterの実装を省略するため、Lombokの`@Data`を利用しています。

**src/main/java/*/domain/Todo.java**

```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**

```java
// (1)
public interface TodoRepository extends CrudRepository<Todo, String> {

   // (2)

   // (3)
   @Query("SELECT COUNT(*) FROM todo WHERE finished = :finished")
   long countByFinished(@Param("finished") boolean finished);
}
```

(1)　RepositoryインターフェイスはCrudRepositoryを継承します。ジェネリクスは`<Domainクラスの型, IDの型>`です。

(2)　CrudRepositoryを継承すると、自動的にCRUDをサポートするデフォルトメソッド（クエリ）が利用可能になります。提供されるメソッドは[CrudRepositoryのJavaDoc](https://docs.spring.io/spring-data/commons/docs/2.2.0.M4/api/org/springframework/data/repository/CrudRepository.html)を確認すると分かります。

(3)　カスタムクエリを実装したければ、`@Query`をメソッドに付与してSQLを記述します。`@Param`を付与した引数が、SQL内の`:パラメータ名`に埋め込まれます。分かりやすいですね。

> Note.
> 更新系のカスタムクエリには、`@Query` + `@Modifying`を付与する必要があります。

#### 複数行のSQL

上ではSQLを1行で記載していますが、可読性が悪いので複数行で記載することをお勧めします。
こんな感じで文字列結合する形になります。

```java
   @Query("SELECT"
           + " COUNT(*)"
           + " FROM todo"
           + " WHERE"
           + " finished = :finished")
   long countByFinished(@Param("finished") boolean finished);
```

#### CRUDのデフォルトメソッドを使わないとき

テーブル構成が複雑でテーブル単位のCRUDを使えないときは、CrudRepositoryの代わりにRepositoryインターフェイスを継承すればOKです。

カスタムクエリを実装する要領で、`@Query`メソッドを実装していきます。

```java
public interface TodoRepository extends Repository<Todo, String> {

   @Query("SELECT ...")
   long findAll();
}
```

#### 動的SQL

Spring Data JDBCでは、動的SQLの組み立てはサポートされていないと思われます。
Spring Data JPAのCrudRepositoryインターフェイスにはExample引数を受け取るメソッドが定義されていますが、Spring DataのCrudRepositoryインターフェイスには定義されておらず、非常に簡素です。

## spring-boot-starter-test

Spring Bootをテストするためのスターターです。

Spring BootのAuto Configurationの仕組みを利用することで、Spring BootアプリをテストするためのBean定義を自動的に行ってくれます。開発者は依存関係にspring-boot-starter-testを追加して、少しの設定をするだけでOKです。

**pom.xml**

```xml
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
```

### JUnitテストケース

JUnitテストケースでは、クラスに`@DataJdbcTest`を付与してテスト対象のリポジトリを`@Autowired`するだけでOKです。

**src/test/java/*/repository/TodoRepositoryTest.java**

```java
@DataJdbcTest
@Transactional
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()));
    }
```

`@DataJdbcTest`はSpring Data JDBCを利用するためのBean定義、DBにアクセスする`DataSource`等のBean定義を自動的に行ってくれます。

`@DataJdbcTest`は`@Transactional`を付与してくれないので、テスト終了後にロールバックするためには**自分で`@Transactional`を付与する必要があります。**`@DataJpaTest`は付与してくれるのに、なぜだ、、、

> Note.
> Spring Boot 2.2.2から`@DataJdbcTest`で`@Transactional`が付与されるようになりました。

## まとめ

Spring Data JDBCのスターターを利用することで、設定周りを省略してRepositoryの実装に注力することができました。

Spring Data JDBCからMyBatisの連携については、別エントリで。