9.

【Spring】DB接続設定から値の取得まで(JdbcTemplate編)

編集
この記事の要点
  • JdbcTemplate は Spring が提供するJDBC のヘルパークラス
  • DB 接続・例外処理・リソース解放を肩代わりし、SQL 実行に集中できる
  • queryForObject / queryForList / query / update を使い分け
  • RowMapper で結果をオブジェクトにマッピング
  • プレースホルダ (?) で SQL インジェクション対策

 

JdbcTemplate の役割

素の JDBC でクエリを実行する場合、Connection 取得・PreparedStatement 作成・ResultSet 処理・例外処理・リソース close を毎回書く必要があり、ボイラープレートが大量に発生します。

JdbcTemplate は Spring がこれらを肩代わりし、SQL 実行に集中できる API を提供します。

設定(Spring Boot)

Spring Boot なら spring-boot-starter-jdbc を依存に追加するだけで、DataSourceJdbcTemplate が自動設定されます。

# application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=secret
spring.datasource.driver-class-name=org.postgresql.Driver

# Connection Pool (HikariCP がデフォルト)
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.connection-timeout=30000
@Repository
public class UserRepository {
    private final JdbcTemplate jdbc;

    public UserRepository(JdbcTemplate jdbc) {  // 自動注入
        this.jdbc = jdbc;
    }
    // ...
}

主要メソッド

① queryForObject - 単一レコード取得

// 単一カラム (count 等)
int count = jdbc.queryForObject("SELECT COUNT(*) FROM users", Integer.class);

// 単一レコード (RowMapper)
User user = jdbc.queryForObject(
    "SELECT id, name, email FROM users WHERE id = ?",
    (rs, rowNum) -> new User(
        rs.getLong("id"),
        rs.getString("name"),
        rs.getString("email")
    ),
    userId
);

// レコードが見つからないと EmptyResultDataAccessException
// 見つかってもよいなら Optional でラップ
public Optional findById(Long id) {
    try {
        User u = jdbc.queryForObject("SELECT ... WHERE id = ?", mapper, id);
        return Optional.of(u);
    } catch (EmptyResultDataAccessException e) {
        return Optional.empty();
    }
}

② query - 複数レコード取得

// RowMapper を使う場合
List users = jdbc.query(
    "SELECT id, name, email FROM users WHERE status = ?",
    (rs, rowNum) -> new User(
        rs.getLong("id"),
        rs.getString("name"),
        rs.getString("email")
    ),
    "ACTIVE"
);

// BeanPropertyRowMapper (フィールド名一致で自動マッピング)
List users = jdbc.query(
    "SELECT id, name, email FROM users",
    new BeanPropertyRowMapper<>(User.class)
);

③ queryForList - Map のリスト

// 各レコードを Map で取得
List> rows = jdbc.queryForList(
    "SELECT id, name FROM users"
);
for (Map row : rows) {
    Long id = (Long) row.get("id");
    String name = (String) row.get("name");
}

// 単一カラムのリスト
List names = jdbc.queryForList(
    "SELECT name FROM users",
    String.class
);

④ update - INSERT / UPDATE / DELETE

// 影響行数を返す
int rows = jdbc.update(
    "UPDATE users SET name = ? WHERE id = ?",
    "Alice",
    1L
);

// INSERT で生成された ID を取得
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbc.update(connection -> {
    PreparedStatement ps = connection.prepareStatement(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        Statement.RETURN_GENERATED_KEYS
    );
    ps.setString(1, "Alice");
    ps.setString(2, "alice@example.com");
    return ps;
}, keyHolder);

Long newId = keyHolder.getKey().longValue();

⑤ batchUpdate - バッチ実行

List users = List.of(new User(1L,"Alice"), new User(2L,"Bob"));
int[] counts = jdbc.batchUpdate(
    "INSERT INTO users (id, name) VALUES (?, ?)",
    users,
    100,  // バッチサイズ
    (ps, user) -> {
        ps.setLong(1, user.getId());
        ps.setString(2, user.getName());
    }
);

NamedParameterJdbcTemplate(名前付きパラメータ)

引数が多いと ? の順番がわかりにくくなります。名前付きパラメータが便利:

@Repository
public class UserRepository {
    private final NamedParameterJdbcTemplate jdbc;

    public UserRepository(NamedParameterJdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public List findByStatusAndRole(String status, String role) {
        String sql = """
            SELECT id, name FROM users
            WHERE status = :status AND role = :role
            """;

        MapSqlParameterSource params = new MapSqlParameterSource()
            .addValue("status", status)
            .addValue("role", role);

        return jdbc.query(sql, params, new BeanPropertyRowMapper<>(User.class));
    }

    public int insert(User user) {
        String sql = "INSERT INTO users (name, email) VALUES (:name, :email)";
        SqlParameterSource params = new BeanPropertySqlParameterSource(user);
        return jdbc.update(sql, params);
    }
}

SimpleJdbcInsert(INSERT 簡略化)

SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbc)
    .withTableName("users")
    .usingGeneratedKeyColumns("id");

Map params = new HashMap<>();
params.put("name", "Alice");
params.put("email", "alice@example.com");

Number id = insert.executeAndReturnKey(params);

トランザクション

@Service
public class UserService {
    private final UserRepository userRepository;
    private final OrderRepository orderRepository;

    @Transactional  // メソッド内の DB 操作をひとつのトランザクションに
    public void registerUser(User user, Order initialOrder) {
        userRepository.insert(user);
        orderRepository.insert(initialOrder);
        // どちらかが失敗すれば両方ロールバック
    }

    @Transactional(readOnly = true)  // 読み取り専用 (パフォーマンス最適化)
    public User findById(Long id) {
        return userRepository.findById(id);
    }
}

例外処理

JdbcTemplate は SQLExceptionDataAccessException 階層に変換します(unchecked になる):

例外意味
EmptyResultDataAccessExceptionqueryForObject で結果が 0 件
IncorrectResultSizeDataAccessExceptionqueryForObject で複数件
DuplicateKeyExceptionUNIQUE 制約違反
DataIntegrityViolationException制約違反 (NOT NULL, FK 等)
DeadlockLoserDataAccessExceptionデッドロック
QueryTimeoutExceptionタイムアウト
BadSqlGrammarExceptionSQL 構文エラー

JdbcTemplate vs JPA / MyBatis

項目JdbcTemplateJPA / HibernateMyBatis
SQL を自分で書く○ 全部書く△ 自動生成 + 一部 JPQL○ 全部書く(XML or アノテーション)
学習コスト
パフォーマンスチューニング容易注意が必要 (N+1 問題等)容易
複雑なクエリ得意苦手得意
オブジェクト指向設計弱い強い
移行コスト (DB 変更)

関連記事

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. インストール(eclipseプラグイン)
  2. クイックスタート
  3. プロジェクトの作成
  4. Spring Bootプロジェクトの作成
  5. Spring Bootプロジェクトの実行
  6. Spring BootでHello World!
  7. アノテーション一覧
  8. DB接続設定からエンティティおよびリポジトリの作成、値の取得まで(JPA編)
  9. DB接続設定や値の取得(JdbcTemplate編)
  10. ビューから値をモデルに格納しコントローラーで受け取る方法
  11. コントローラーにてモデルに値を格納してビューに渡す方法
  12. テンプレートエンジン
  13. ModelとModelAndViewの違い
  14. AOPの使用方法
  15. classpath: 内部ファイルの読み込み
  16. file: 外部ファイルの読み込み
  17. CSVファイルアップロード方法(Ajax)
  18. CSVファイルダウンロード方法(Ajax)
  19. Spring Bootプロジェクトのビルドと本番環境へのデプロイ方法(内部tomcat使用)
  20. Application.propertiesの環境依存設定の分割方法
  21. JPAにおけるEntityManagerの取得方法
  22. JPAにおけるjava.sql.Connectionの取得方法
  23. エラー一覧
  24. jarの引数を受け取る方法
  25. Spring BootでGmailからメール送信
  26. 複数のDBに接続する設定(Spring Boot & JPA編)
  27. ポート番号の変更
  28. Basic認証の実装と特定のURLに限定する方法
  29. Spring SecurityのBasic認証の無効化
  30. 独自のエラーページを定義する方法
  31. プロパティファイルの値やjar実行時の引数を取得する方法