【SpringBoot】JdbcTemplateとSpring Data JPAの違いを整理してみた
おはようございます!DWSのkimです!
最近はAWSから少し離れて、JavaやSpring Bootを学習しています。
これから少しずつJavaに関する内容もブログにまとめていければと思います。
今回は、JdbcTemplateとSpring Data JPAの違いについて整理します。
Spring Bootを学習し始めたころにJdbcTemplateを利用する機会があり、その後、案件ではSpring Data JPAを扱う機会がありました。
両方を利用すると、SQLを直接書くJdbcTemplateと、Entityを中心に扱うSpring Data JPAでは、実装方針や記述量に明確な違いがありました。
この記事では、CRUD処理や条件付き検索の例を通して、それぞれの違いを整理していきます。
1. JdbcTemplateとは?
JavaにはJDBC(Java Database Connectivity)という、Javaから各データベース(MySQLやPostgreSQLなど)に接続して、SQLを実行するための仕組みがあります。

Spring Bootでは、データベースアクセスを扱う方法として、JdbcTemplate、MyBatis、JPA / Spring Data JPA などの選択肢があります。
その中でもJdbcTemplateは、JDBCを直接扱う場合に発生しがちな接続処理や例外処理などを、Springで扱いやすくするためのクラスです。
2. JPA / Spring Data JPAとは?
JPA(Java Persistence API)は、JavaのEntityクラスとデータベースのテーブルを対応させ、Javaオブジェクトを扱うようにデータの登録・取得・更新・削除を行うための仕組みです。
JdbcTemplateではSQLを直接書いてデータベースを操作します。
一方、JPA / Spring Data JPAでは、Entityと呼ばれるJavaクラスを通してデータベースを操作します。
さらにSpring Bootでは、Spring Data JPAを使うことでRepositoryの実装を大きく簡略化できます。たとえば、JpaRepositoryを継承するだけで、基本的なCRUD処理をすぐに使えるようになります。
どれくらい記述量が変わるのか、次の章から具体的なコードで見ていきます。
3. JdbcTemplateとSpring Data JPAを比較してみる
ECサイトの注文管理を例に、JdbcTemplateとSpring Data JPAでデータベース操作のコードがどのように変わるのかを比較していきます。
3-1. 一般的なCRUD処理について
まずは一般的なCRUD処理(全件取得・1件取得・登録・更新・削除)について、JdbcTemplateではどのように実装するのかを見ていきます。
3-1-1. JdbcTemplateで書く一般的なCRUD処理
少し長いですが、JdbcTemplateでCRUD処理を書く場合は、SQLの定義やResultSetからEntityへの変換処理も自分で書く必要があります…
@Repository
public class OrderRepositoryJdbc {
private final JdbcTemplate jdbcTemplate;
public OrderRepositoryJdbc(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 【全件取得】SELECT * FROM orders
public List<Order> findAll() {
String sql = "SELECT id, customer_name, status, created_at, updated_at FROM orders";
return jdbcTemplate.query(sql, this::mapRow); // 全行を mapRow で変換
}
// 【1件取得】SELECT ... WHERE id = ?
public Optional<Order> findById(Long id) {
String sql = "SELECT id, customer_name, status, created_at, updated_at "
+ "FROM orders WHERE id = ?";
List<Order> result = jdbcTemplate.query(sql, this::mapRow, id);
return result.stream().findFirst(); // 空なら Optional.empty() を返す
}
// 【登録】INSERT INTO orders ...
public Order save(Order order) {
String sql = "INSERT INTO orders (customer_name, status, created_at, updated_at) "
+ "VALUES (?, ?, ?, ?)";
// KeyHolder で採番された ID を受け取る
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(conn -> {
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, order.getCustomerName());
ps.setString(2, order.getStatus());
ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
ps.setTimestamp(4, Timestamp.valueOf(LocalDateTime.now()));
return ps;
}, keyHolder);
// 生成された ID を Order にセットして返す
order.setId(Objects.requireNonNull(keyHolder.getKey()).longValue());
return order;
}
// 【更新】UPDATE orders SET ... WHERE id = ?
public Order update(Order order) {
String sql = "UPDATE orders SET customer_name = ?, status = ?, updated_at = ? "
+ "WHERE id = ?";
jdbcTemplate.update(sql,
order.getCustomerName(),
order.getStatus(),
Timestamp.valueOf(LocalDateTime.now()),
order.getId()
);
return order;
}
// 【削除】DELETE FROM orders WHERE id = ?
public void deleteById(Long id) {
jdbcTemplate.update("DELETE FROM orders WHERE id = ?", id);
}
// ResultSet(DB の 1 行)を Order オブジェクトに変換
private Order mapRow(ResultSet rs, int rowNum) throws SQLException {
Order order = new Order();
order.setId(rs.getLong("id"));
order.setCustomerName(rs.getString("customer_name"));
order.setStatus(rs.getString("status"));
order.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
order.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
return order;
}
}3-1-2. Spring Data JPAで書く一般的なCRUD処理
※Spring Data JPAは、SpringでJPAをより簡単に扱えるようにする仕組みです。
public interface OrderRepository extends JpaRepository<Order, Long> {}「ほぼ、何も書かれていないじゃないか???」と思われたと思います。
実はSpring Data JPAでは、基本的なCRUD処理があらかじめ用意されています。
RepositoryインターフェースでJpaRepositoryを継承するだけで、以下のような機能をすぐに利用できます。
| メソッド | 何をするか |
|---|---|
findAll() | 全件取得 |
findById(Long id) | 1件取得 |
save(Order order) | 登録・更新 |
delete(Order order) | 指定したEntityを削除 |
deleteById(Long id) | IDを指定して削除 |
count() | 件数取得 |
existsById(Long id) | 指定したIDのデータが存在するか確認 |
ただ、これ以外にももっと細かい条件で検索・削除・存在確認をしたい場面もあります。
そのような場合に、Spring Data JPAではどこまで簡単に書けるのかを見ていきます。
3-2. 条件付きの取得・削除・存在確認
基本的なCRUD処理だけでなく、実際のアプリケーションでは条件付きで取得・削除・存在確認を行いたい場面もあります。
ここでは、そのような処理をJdbcTemplateとSpring Data JPAでどのように書くのかを比較します。
| 処理 | シナリオ |
|---|---|
findByUser_Id | ユーザーの投稿一覧ページを開いたとき、そのユーザーの投稿を全件取得する |
findByUser_IdIn | 複数ユーザーの投稿をまとめて取得する。タイムライン表示など |
deleteByUser_Id | ユーザーを退会させるとき、そのユーザーの投稿をすべて削除する |
existsByCategory_Id | カテゴリを削除しようとしたとき、そのカテゴリに投稿が紐づいていないか確認する |
※ findByUser_Id は、Postエンティティが User エンティティを関連として持っている場合の書き方です。
Postに userId というフィールドを直接持っている場合は、findByUserId のような命名になります。
3-2-1. JdbcTemplateの条件付きの取得・削除・存在確認
こちらも先ほど同様にコードが少し長いですが、JdbcTemplateでこれらの書く場合は、SQLの定義やResultSetからEntityへの変換処理も自分で書く必要があります…
@Repository
public class PostRepositoryJdbc {
private final JdbcTemplate jdbcTemplate;
public PostRepositoryJdbc(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// ① ユーザーの投稿一覧を取得
public List<Post> findByUserId(Long userId) {
String sql = """
SELECT id, user_id, category_id,
title, body, created_at, updated_at
FROM posts
WHERE user_id = ?
""";
// SQL を実行し、各行を mapRow で Post オブジェクトに変換
return jdbcTemplate.query(sql, this::mapRow, userId);
}
// ② 複数ユーザーの投稿をまとめて取得
public List<Post> findByUserIdIn(List<Long> userIds) {
// IN (?, ?, ?) を userIds の件数分だけ動的に組み立てる
String placeholders = userIds.stream()
.map(id -> "?")
.collect(Collectors.joining(","));
String sql = "SELECT id, user_id, category_id, title, body, created_at, updated_at "
+ "FROM posts WHERE user_id IN (" + placeholders + ")";
return jdbcTemplate.query(sql, this::mapRow, userIds.toArray());
}
// ③ ユーザーの投稿を全件削除
public void deleteByUserId(Long userId) {
jdbcTemplate.update(
"DELETE FROM posts WHERE user_id = ?",
userId
);
}
// ④ カテゴリに投稿が紐づいているか確認
public boolean existsByCategoryId(Long categoryId) {
// COUNT(*) で件数を取得し、1件でもあれば true を返す
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM posts WHERE category_id = ?",
Integer.class,
categoryId
);
return count != null && count > 0; // null ガードしないと NullPointerException が起きうる
}
// ResultSet(DB から返ってきた 1 行)を Post オブジェクトに変換
private Post mapRow(ResultSet rs, int rowNum) throws SQLException {
Post post = new Post();
post.setId(rs.getLong("id"));
post.setTitle(rs.getString("title"));
post.setBody(rs.getString("body"));
post.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
post.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
// user_id / category_id から関連エンティティを取り直す処理も別途必要になりがち…
return post;
}
}3-2-2. Spring Data JPAの条件付きの取得・削除・存在確認
public interface PostRepository extends JpaRepository<Post, Long> {
// ① ユーザーの投稿一覧を取得
List<Post> findByUser_Id(Long userId);
// ② 複数ユーザーの投稿をまとめて取得(IN 句)
List<Post> findByUser_IdIn(List<Long> userIds);
// ③ ユーザーの投稿を全件削除
void deleteByUser_Id(Long userId);
// ④ カテゴリに投稿が紐づいているか確認
boolean existsByCategory_Id(Long categoryId);
}Spring Data JPA側にもメソッド定義は必要ですが、JdbcTemplateと比較すると記述量を抑えられることが分かります。
Spring Data JPAには、Repositoryのメソッド名から必要なクエリを生成するDerived Queryという仕組みがあります。
例えば、findByUser_IdInを見てみますと、Spring Data JPAは下記のように「find なので取得処理」「By 以降が検索条件」「In なのでIN句」といった形で、メソッド名から処理内容を判断し、クエリを生成してくれます。
例: findByUser_IdIn
└─ find → 取得処理
└─ By → ここから検索条件
└─ User_Id → user.id を条件にする
└─ In → IN句次の章では、このDerived Queryの命名規則を整理します。
4. Derived Query の命名規則
4-1. 基本構造
[動詞]By[フィールド名][条件]
↑ ↑
何をするか 何で絞り込むか基本構造は上記の通りです。
次に、先頭に付ける動詞や、Byの後ろに付ける条件キーワードを見ていきます。
4-2. 動詞:先頭に付けるもの
| 動詞 | 戻り値 | 例 | 意味 |
|---|---|---|---|
findBy | List<T> / Optional<T> | findByName | 条件に一致するデータを取得する |
existsBy | boolean | existsByEmail | 条件に一致するデータが存在するか確認する |
countBy | long | countByStatus | 条件に一致する件数を数える |
deleteBy | void | deleteByUser_Id | 条件に一致するデータを削除する |
4-3. 条件キーワード:By の後ろに付けるもの
4-3-1. 比較系
| キーワード | 意味 | 例 | 生成されるSQL |
|---|---|---|---|
| なし | 完全一致 | findByName(String name) | WHERE name = ? |
Not | 否定 | findByNameNot(String name) | WHERE name != ? |
Like | 部分一致。% を自分で付ける | findByNameLike(String name) | WHERE name LIKE ? |
Containing | 部分一致。% を自動で付ける | findByNameContaining(String name) | WHERE name LIKE %?% |
StartingWith | 前方一致 | findByNameStartingWith(String name) | WHERE name LIKE ?% |
EndingWith | 後方一致 | findByNameEndingWith(String name) | WHERE name LIKE %? |
IgnoreCase | 大文字小文字を無視 | findByNameIgnoreCase(String name) | WHERE UPPER(name) = UPPER(?) |
4-3-2. 数値・日付
| キーワード | 意味 | 例 | 生成されるSQL |
|---|---|---|---|
LessThan | より小さい | findByAgeLessThan(int age) | WHERE age < ? |
LessThanEqual | 以下 | findByAgeLessThanEqual(int age) | WHERE age <= ? |
GreaterThan | より大きい | findByAgeGreaterThan(int age) | WHERE age > ? |
GreaterThanEqual | 以上 | findByAgeGreaterThanEqual(int age) | WHERE age >= ? |
Between | 範囲 | findByAgeBetween(int from, int to) | WHERE age BETWEEN ? AND ? |
4-3-3. NULL系
| キーワード | 意味 | 例 | 生成されるSQL |
|---|---|---|---|
IsNull | NULL のデータを取得 | findByDeletedAtIsNull() | WHERE deleted_at IS NULL |
IsNotNull | NULL ではないデータを取得 | findByDeletedAtIsNotNull() | WHERE deleted_at IS NOT NULL |
4-3-4. コレクション系
| キーワード | 意味 | 例 | 生成されるSQL |
|---|---|---|---|
In | IN 句 | findByUser_IdIn(List<Long> ids) | WHERE user_id IN (?, ?, ?) |
NotIn | NOT IN 句 | findByStatusNotIn(List<String> statuses) | WHERE status NOT IN (?, ?) |
※Derived Queryで利用できるキーワードは、Spring Data JPAの公式ドキュメントにも一覧があります。
本記事では、その中でもよく使いそうなものを抜粋して整理しています。
詳細については以下の公式ページをご確認ください
https://docs.spring.io/spring-data/jpa/reference/repositories/query-keywords-reference.html
まとめ
今回は、JdbcTemplateとSpring Data JPAを比較しながら、データベース操作の書き方の違いを整理しました。
Spring Data JPAは、基本的なCRUD処理や単純な条件検索を少ないコードで実装できるため、開発効率を上げやすいというメリットがあります。
ただし、「簡単に書けるから全部Spring Data JPAで良い」というわけではありません。
大量データの一括更新・一括削除や、複雑な集計SQLを扱う場合は、JdbcTemplateやネイティブSQLを使った方が分かりやすいこともあります。
それぞれの特徴を理解したうえで、処理内容に応じて適切に使い分けていきたいと思います。

