はじめに
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コンテナがある環境でも利用可能です。
詳細は別資料👇をご参照ください。