36
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-17

はじめに

JavaのORマッパーは何をお使いでしょうか?

最近の僕は、以下の理由からSpringのJdbcTemplateクラスをよく使っています。

  • 設定ファイル不要なので、簡単に利用開始できる!
  • 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定義やインスタンス生成を自分で書く必要はありません)。

@Repository
public class SampleRepository {
    private final JdbcTemplate jdbcTemplate;

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

    ...
}

SpringのDIコンテナを利用しているが、Spring Bootは利用していない場合

JdbcTemplateをBean定義後、DIで取得します。

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

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
@Repository
public class SampleRepository {
    private final JdbcTemplate jdbcTemplate;

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

    ...
}

SpringのDIコンテナを利用していない場合

DataSource dataSource = ...;
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

設定

タイムアウト時間

TBD

フェッチサイズ

TBD

最大行数

TBD

サンプルデータベース

テーブル作成
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レコードは次のように作成します。

import java.time.LocalDateTime;

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

検索の実行

1件検索

DBから1件だけデータを検索するにはqueryForObject()メソッドを利用します。

引数は次の通りです。

  • 第1引数: SELECT文
  • 第2引数: RowMapperインタフェース
    • ラムダ式でResultSetを任意の型(今回はSample)に変換する処理を記述します。
  • 第3引数以降(可変長引数): SELECT文内の?に当てはめる値を順番に指定
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);
実行結果
Sample[id=1, message=あああ, createdAt=2023-05-17T16:38:22.054234]

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

集計関数の結果を取得

COUNT()などの集計関数を利用する場合は、先程とは別のqueryForObject()メソッドを利用します。

int count = jdbcTemplate.queryForObject(
        """
        SELECT
           COUNT(*)
        FROM
            sample
        """, Integer.class);
System.out.println(count);
実行結果
3

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

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

複数件検索

DBからデータを複数件検索するにはquery()メソッドを利用します。

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);
実行結果
[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()メソッドを利用して少しずつデータを読み込みましょう。

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

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();
}
実行結果
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というRowMapper実装クラスを利用すると、SELECT結果の列名とレコードのコンポーネント名を比較して、同じものに代入してくれます。created_atcreatedAtのような「スネークケース→キャメルケース」の変換も行ってくれます。

List<Sample> list = jdbcTemplate.query(
        """
        SELECT
           id,
           message,
           created_at
        FROM
            sample
        """,
        new DataClassRowMapper<>(Sample.class));
System.out.println(list);
実行結果
[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ファイルを読み込んで実行することができます。

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

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

追加・更新・削除の実行

単純な実行

INSERT文・UPDATE文・DELETE文を実行するにはupdate()メソッドを利用します。

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

int rows = jdbcTemplate.update("""
        INSERT INTO sample(message, created_at)
        VALUES(?, ?)
        """,
        "えええ",
        LocalDateTime.now());
System.out.println(rows);
実行結果
1

生成されたID値の取得

主キー値がシーケンスなどで自動生成されている場合で、INSERT時にその値を取得したい場合は、別のupdate()メソッドを利用します。

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);
実行結果
1
4

バッチ実行

TBD

DDLの実行

CREATE文などのDDLを実行する場合はexecute()メソッドを利用します。

PostgreSQLのVACUUMANALYZEもこのメソッドで実行できます。

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

NamedParameterJdbcTemplateクラスの利用

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

そこでNamedParameterJdbcTemplateクラスの登場です。このクラスにはJdbcTemplateクラスとほぼ同じメソッドが用意されています(引数が微妙に違うので注意)。しかし、パラメーターを?ではなく:パラメーター名で指定することができます。

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

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);
クラスのプロパティでパラメーター値を指定
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);
実行結果
Sample[id=1, message=あああ, createdAt=2023-05-30T10:36:29.878862]

queryForObject()の他、query()update()も用意されています。

execute()も用意されてはいるものの、JdbcTemplateのものとだいぶ使い方が違うので注意してください。

発生する例外

JdbcTemplateの各メソッドで発生する例外は、全てDataAccessExceptionのサブクラスです。

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

トランザクション管理

SpringのDIコンテナがある環境では、@Transactionalアノテーションを利用できます。

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

SpringのDIコンテナが無い環境では、TransactionTemplateクラスを利用します。

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

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

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

36
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?