バックエンド

【SpringBoot】JdbcTemplateとSpring Data JPAの違いを整理してみた

kim

おはようございます!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. 動詞:先頭に付けるもの

動詞戻り値意味
findByList<T> / Optional<T>findByName条件に一致するデータを取得する
existsBybooleanexistsByEmail条件に一致するデータが存在するか確認する
countBylongcountByStatus条件に一致する件数を数える
deleteByvoiddeleteByUser_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
IsNullNULL のデータを取得findByDeletedAtIsNull()WHERE deleted_at IS NULL
IsNotNullNULL ではないデータを取得findByDeletedAtIsNotNull()WHERE deleted_at IS NOT NULL

4-3-4. コレクション系

キーワード意味生成されるSQL
InINfindByUser_IdIn(List<Long> ids)WHERE user_id IN (?, ?, ?)
NotInNOT INfindByStatusNotIn(List<String> statuses)WHERE status NOT IN (?, ?)

※Derived Queryで利用できるキーワードは、Spring Data JPAの公式ドキュメントにも一覧があります。
本記事では、その中でもよく使いそうなものを抜粋して整理しています。

詳細については以下の公式ページをご確認ください

Repository query keywords

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を使った方が分かりやすいこともあります。

それぞれの特徴を理解したうえで、処理内容に応じて適切に使い分けていきたいと思います。

AUTHOR
kim
kim
記事URLをコピーしました