最強ORマッパー？SpringのJdbcTemplateは意外と出来る子

# はじめに
JavaのORマッパーは何をお使いでしょうか？

最近の僕は、以下の理由からSpringの[`JdbcTemplate`クラス](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html)をよく使っています。

- 設定ファイル不要なので、簡単に利用開始できる！
- SpringのDIコンテナが無い環境でも利用できる！
- 内部でやっていることは単純であるため、ハマりにくい！

# 環境
- JDK 17
- Spring JDBC 6.0

> テスト用データベースにはH2を利用しました。しかしこの記事の内容は、一部を除いてRDBの種類に関係なく適用できるはずです。

# インスタンス生成
`JdbcTemplate`のインスタンス生成は簡単です。コンストラクタの引数に`DataSource`を渡すだけです。

> `DataSource`は、コネクションを取得する際の窓口となるインタフェースです。実装クラスとしてはHikariCPの`HikariDataSource`（コネクションプール機能付き）や、Springの`DriverManagerDataSource`（コネクションプール機能無し）などがあります。

## Spring Bootを利用している場合
Spring Bootを利用している場合は、Bean定義済みの`JdbcTemplate`をDIで取得します（＝後述のようなBean定義やインスタンス生成を自分で書く必要はありません）。

```java
@Repository
public class SampleRepository {
    private final JdbcTemplate jdbcTemplate;

    public SampleRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    ...
}
```

## SpringのDIコンテナを利用しているが、Spring Bootは利用していない場合
`JdbcTemplate`をBean定義後、DIで取得します。

```java
@Configuration
public class RepositoryConfig {
    @Bean
    public DataSource dataSource() {
        return ...;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
```

```java
@Repository
public class SampleRepository {
    private final JdbcTemplate jdbcTemplate;

    public SampleRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    ...
}
```

## SpringのDIコンテナを利用していない場合
```java
DataSource dataSource = ...;
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
```

# 設定
## タイムアウト時間
TBD

## フェッチサイズ
TBD

## 最大行数
TBD

# サンプルデータベース
```sql:テーブル作成
CREATE TABLE sample(
    id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    message VARCHAR(32) NOT NULL,
    created_at TIMESTAMP NOT NULL
);

INSERT INTO sample VALUES(DEFAULT, 'あああ', CURRENT_TIMESTAMP);
INSERT INTO sample VALUES(DEFAULT, 'いいい', CURRENT_TIMESTAMP);
INSERT INTO sample VALUES(DEFAULT, 'ううう', CURRENT_TIMESTAMP);
```

> このSQLはH2向けなので、他のRDBで実行する際は適切に書き換えてください。

このテーブルに対応する`Sample`レコードは次のように作成します。

```java
import java.time.LocalDateTime;

public record Sample(Integer id, String message, LocalDateTime createdAt) {
}
```

# 検索の実行
## 1件検索
DBから1件だけデータを検索するには[`queryForObject()`メソッド](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#queryForObject(java.lang.String,org.springframework.jdbc.core.RowMapper,java.lang.Object...))を利用します。

引数は次の通りです。

- 第1引数: SELECT文
- 第2引数: [`RowMapper`インタフェース](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/RowMapper.html)
    - ラムダ式で`ResultSet`を任意の型（今回は`Sample`）に変換する処理を記述します。
- 第3引数以降（可変長引数）: SELECT文内の`?`に当てはめる値を順番に指定

```java
Sample sample = jdbcTemplate.queryForObject(
        """
        SELECT
           id,
           message,
           created_at
        FROM
            sample
        WHERE
            id = ?
        AND
            message = ?
        """, (rs, rowNum) ->
        new Sample(
                rs.getInt("id"),
                rs.getString("message"),
                rs.getObject("created_at", LocalDateTime.class)
        ),
        1,
        "あああ");
System.out.println(sample);
```

```text:実行結果
Sample[id=1, message=あああ, createdAt=2023-05-17T16:38:22.054234]
```

検索結果が1件でない場合（0件または複数件）だった場合は`IncorrectResultSizeDataAccessException`がスローされます。

## 集計関数の結果を取得
`COUNT()`などの集計関数を利用する場合は、先程とは別の[`queryForObject()`メソッド](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#queryForObject(java.lang.String,java.lang.Class,java.lang.Object...))を利用します。

```java
int count = jdbcTemplate.queryForObject(
        """
        SELECT
           COUNT(*)
        FROM
            sample
        """, Integer.class);
System.out.println(count);
```

```text:実行結果
3
```

検索結果が1件でない場合（0件または複数件）だった場合は`IncorrectResultSizeDataAccessException`がスローされます。

> WHERE句にパラメータ`?`がある場合は、第3引数以降に可変長引数で指定します。

## 複数件検索
DBからデータを複数件検索するには[`query()`メソッド](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#query(java.lang.String,org.springframework.jdbc.core.RowMapper,java.lang.Object...))を利用します。

```java
List<Sample> list = jdbcTemplate.query(
        """
        SELECT
           id,
           message,
           created_at
        FROM
            sample
        """, (rs, rowNum) ->
        new Sample(
                rs.getInt("id"),
                rs.getString("message"),
                rs.getObject("created_at", LocalDateTime.class)
        ));
System.out.println(list);
```

```text:実行結果
[Sample[id=1, message=あああ, createdAt=2023-05-17T16:58:31.143445], Sample[id=2, message=いいい, createdAt=2023-05-17T16:58:31.146347], Sample[id=3, message=ううう, createdAt=2023-05-17T16:58:31.146709]]
```

> WHERE句にパラメータ`?`がある場合は、第3引数以降に可変長引数で指定します。

## Stream APIの利用
`query()`メソッドを利用すると、全ての検索結果を一気に読み込みます。テーブル内のレコードが大量の場合、これにより`OutOfMemoryError`が発生する可能性があります。

データが大量にある場合は、[`queryForStream()`メソッド](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#queryForStream(java.lang.String,org.springframework.jdbc.core.RowMapper,java.lang.Object...))を利用して少しずつデータを読み込みましょう。

処理終了後に`Stream`をクローズしないと、DBコネクションもクローズされません。クローズ忘れを防ぐためには、try-with-resourcesを使います。

```java
try (Stream<Sample> stream = jdbcTemplate.queryForStream(
        """
        SELECT
           id,
           message,
           created_at
        FROM
            sample
        """,
        new DataClassRowMapper<>(Sample.class))) {
    stream.forEach(sample -> System.out.println(sample));
} catch (DataAccessException e) {
    e.printStackTrace();
}
```

```text:実行結果
Sample[id=1, message=あああ, createdAt=2023-05-17T17:10:00.401613]
Sample[id=2, message=いいい, createdAt=2023-05-17T17:10:00.403474]
Sample[id=3, message=ううう, createdAt=2023-05-17T17:10:00.403792]
```

> WHERE句にパラメータ`?`がある場合は、第3引数以降に可変長引数で指定します。

## `DataClassRowMapper`でもっと簡単に記述
ここまで読んだ方は、「`ResultSet`から`Sample`に変換するのが面倒」と思ったのではないでしょうか。

[`DataClassRowMapper`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/DataClassRowMapper.html)という`RowMapper`実装クラスを利用すると、SELECT結果の列名とレコードのコンポーネント名を比較して、同じものに代入してくれます。`created_at`と`createdAt`のような「スネークケース→キャメルケース」の変換も行ってくれます。

```java
List<Sample> list = jdbcTemplate.query(
        """
        SELECT
           id,
           message,
           created_at
        FROM
            sample
        """,
        new DataClassRowMapper<>(Sample.class));
System.out.println(list);
```

```text:実行結果
[Sample[id=1, message=あああ, createdAt=2023-05-17T16:58:31.143445], Sample[id=2, message=いいい, createdAt=2023-05-17T16:58:31.146347], Sample[id=3, message=ううう, createdAt=2023-05-17T16:58:31.146709]]
```

> WHERE句にパラメータ`?`がある場合は、第3引数以降に可変長引数で指定します。

とても便利なので、基本的には`DataClassRowMapper`の利用をおすすめします。列名とコンポーネント名が異なる場合のみ、`ResultSet`からの変換処理を書きましょう。

## doma-templateで2way-SQL化
「SQLをプログラム内に文字列で書くのが面倒！」と思った方も多いでしょう。

doma-templateという別のライブラリを利用すると、次のような2way-SQLなSQLファイルを読み込んで実行することができます。

```sql:2way-SQLファイル
select
    *
from
    emp
where
    name = /* name */''
and
    salary = /* salary */0
```

詳細は👇の記事を参照してください。

https://qiita.com/suke_masa/items/50406541841461dee773

# 追加・更新・削除の実行
## 単純な実行
INSERT文・UPDATE文・DELETE文を実行するには[`update()`メソッド](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#update(java.lang.String,java.lang.Object...))を利用します。

戻り値は更新した行数です。例えば、INSERT文で1行追加した時は`1`が返ります。

```java
int rows = jdbcTemplate.update("""
        INSERT INTO sample(message, created_at)
        VALUES(?, ?)
        """,
        "えええ",
        LocalDateTime.now());
System.out.println(rows);
```

```text:実行結果
1
```

## 生成されたID値の取得
主キー値がシーケンスなどで自動生成されている場合で、INSERT時にその値を取得したい場合は、別の[update()メソッド](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#update(org.springframework.jdbc.core.PreparedStatementCreator,org.springframework.jdbc.support.KeyHolder))を利用します。

```java
KeyHolder keyHolder = new GeneratedKeyHolder();
int rows = jdbcTemplate.update(con -> {
    PreparedStatement ps = con.prepareStatement("""
            INSERT INTO sample(message, created_at)
            VALUES(?, ?)
            """,
            // 主キー列名を指定
            new String[]{"id"});
    ps.setString(1, "えええ");
    ps.setObject(2, LocalDateTime.now());
    return ps;
}, keyHolder);
// 生成された主キー値を取得
int newId = keyHolder.getKey().intValue();
System.out.println(rows);
System.out.println(newId);
```

```text:実行結果
1
4
```

## バッチ実行
TBD

# DDLの実行
CREATE文などのDDLを実行する場合は[`execute()`メソッド](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#execute(java.lang.String))を利用します。

PostgreSQLの`VACUUM`や`ANALYZE`もこのメソッドで実行できます。

```java
jdbcTemplate.execute("""
    CREATE TABLE sample(
        id INTEGER PRIMARY KEY,
        message VARCHAR(32) NOT NULL
    )
    """);
```

# `NamedParameterJdbcTemplate`クラスの利用
JdbcTemplateの難点は、パラメーターを`?`で記述することです。特にパラメーターが多い場合、`query()`や`queryForObject()`の最後に可変長引数で指定するパラメーター値と、`?`の個数および順番に注意する必要があるので大変です。

そこで[`NamedParameterJdbcTemplate`クラス](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html)の登場です。このクラスには`JdbcTemplate`クラスとほぼ同じメソッドが用意されています（引数が微妙に違うので注意）。しかし、パラメーターを`?`ではなく`:パラメーター名`で指定することができます。

パラメーター値は、`Map`やクラスのプロパティで指定可能です。

```java:Mapでパラメーター値を指定
Sample sample = namedParameterJdbcTemplate.queryForObject("""
        SELECT
           id,
           message,
           created_at
        FROM
            sample
        WHERE
            id = :id
        AND
            message = :message
        """,
        Map.of("id", 1,
               "message", "あああ"),
        new DataClassRowMapper<>(Sample.class));
System.out.println(sample);
```

```java:クラスのプロパティでパラメーター値を指定
record SampleParam(Integer id, String message, LocalDateTime createdAt) {
}

Sample sample = namedParameterJdbcTemplate.queryForObject("""
        SELECT
           id,
           message,
           created_at
        FROM
            sample
        WHERE
            id = :id
        AND
            message = :message
        """,
        new BeanPropertySqlParameterSource(new SampleParam(1, "あああ", null)),
        new DataClassRowMapper<>(Sample.class));
System.out.println(sample);
```

```text:実行結果
Sample[id=1, message=あああ, createdAt=2023-05-30T10:36:29.878862]
```

[`queryForObject()`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html#queryForObject(java.lang.String,java.util.Map,org.springframework.jdbc.core.RowMapper))の他、[`query()`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html#query(java.lang.String,java.util.Map,org.springframework.jdbc.core.RowMapper))・[`update()`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html#update(java.lang.String,java.util.Map))も用意されています。

[`execute()`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html#execute(java.lang.String,org.springframework.jdbc.core.PreparedStatementCallback))も用意されてはいるものの、`JdbcTemplate`のものとだいぶ使い方が違うので注意してください。

# 発生する例外
`JdbcTemplate`の各メソッドで発生する例外は、全て[`DataAccessException`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/dao/DataAccessException.html)のサブクラスです。

この例外は非チェック例外であるため、`try-catch`や`throws`による例外処理は必須ではありません。WebフレームワークSpring MVCと併用している場合は、コントローラーなどの各メソッドでの例外処理は行わず、`@ExceptionHandler`が付加されたメソッドで例外処理を行うことが多いです。

# トランザクション管理
SpringのDIコンテナがある環境では、[`@Transactional`アノテーション](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html)を利用できます。

```java
@Transactional
public void doSomething() {
    jdbcTemplate.update(...);
    jdbcTemplate.update(...);
}
```

SpringのDIコンテナが無い環境では、[`TransactionTemplate`クラス](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/support/TransactionTemplate.html)を利用します。

```java
TransactionTemplate txTemplate = ...;
txTemplate.execute(status -> {
    jdbcTemplate.update(...);
    jdbcTemplate.update(...);
    return ...;
});
```

> `TransactionTemplate`はDIコンテナがある環境でも利用可能です。

詳細は別資料👇をご参照ください。

<script async class="docswell-embed" src="https://www.docswell.com/assets/libs/docswell-embed/docswell-embed.min.js" data-src="https://www.docswell.com/slide/Z8GG2E/embed" data-aspect="0.75"></script><div class="docswell-link"><a href="https://www.docswell.com/s/MasatoshiTada/Z8GG2E-spring-transaction-explained">詳解Springトランザクション -初級から上級まで- #jsug by @MasatoshiTada</a></div>