はじめに
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_atとcreatedAtのような「スネークケース→キャメルケース」の変換も行ってくれます。
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ファイルを読み込んで実行することができます。
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のVACUUMやANALYZEもこのメソッドで実行できます。
jdbcTemplate.execute("""
CREATE TABLE sample(
id INTEGER PRIMARY KEY,
message VARCHAR(32) NOT NULL
)
""");
NamedParameterJdbcTemplateクラスの利用
JdbcTemplateの難点は、パラメーターを?で記述することです。特にパラメーターが多い場合、query()やqueryForObject()の最後に可変長引数で指定するパラメーター値と、?の個数および順番に注意する必要があるので大変です。
そこでNamedParameterJdbcTemplateクラスの登場です。このクラスにはJdbcTemplateクラスとほぼ同じメソッドが用意されています(引数が微妙に違うので注意)。しかし、パラメーターを?ではなく:パラメーター名で指定することができます。
パラメーター値は、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-catchやthrowsによる例外処理は必須ではありません。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コンテナがある環境でも利用可能です。
詳細は別資料👇をご参照ください。