Post

(Spring JDBC) JdbcOperations

JdbcOperations 인터페이스와 구현체

1
2
3
4
class JdbcTemplate: JdbcOperations
// 기본 기능
class NamedParameterJdbcTemplate: NamedParameterJdbcOperations
// 기본 기능 + 쿼리 내에서 :param 으로 이름 지정한 바인딩 사용 가능
  • 보통 NamedParameterJdbcOperations를 DI 받아 사용하게 됨.
  • JdbcTemplate 같은 구현체는 SpringBoot에서 Auto-config로 생성해준다.

JdbcOperations 예제

기본적인 사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
// query 결과가 없는 경우 EmptyResultDataAccessException가 발생한다. DAO 내에서 try-catch해주자.

Product product = jdbcOperations.queryForObject(
    query, 
    new Object[] { productCode }, 
    new BeanPropertyRowMapper<>(Product.class)
);

List<DisposalHistory> disposalHistoryList = jdbcOperations.query(
    query, 
    new Object[] { dateDate }, 
    new BeanPropertyRowMapper<>(DisposalHistory.class)
);

반환 타입

queryForList

1
2
3
List<Map<String, Any>>
[{id = 1, name="qer"} , {id = 2, name="asdf"}, ...]
// (id, name이 컬럼 이름이다.)

queryForMap

1
2
3
4
5
6
7
8
9
Map<K, Map<String, Any>>
// id 컬럼을 K로 지정한 경우
{
    1 : {id = 1, name="qer"},
    2 : {id = 2, name="asdf"},
}

// 결과로 반환받은 행 중에 id = 1인 레코드의 name 필드에 접근하려면
result.get(1).get(name)

레코드를 INSERT하는 동시에 해당 레코드의 Key값( sequence )을 반환받기

KeyHolder와 PreparedStatement를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String query = "INSERT INTO SALES(sales_id, customer_id, branch_id, sales_time, receipt_id, cancel_check, amount) "
    + "VALUES(sales_id_seq.nextval, ?, ?, TO_DATE(?,'YYYYMMDD HH24:MI:SS'), sales_id_seq.currval)";

  
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
    PreparedStatement ps = connection.prepareStatement(query, PreparedStatement.RETURN_GENERATED_KEYS);
    ps.setString(1, (sales.getCustomerId() != null) ? sales.getCustomerId().toString() : "NULL");
    ps.setString(2,Long.toString(sales.getBranchId()));
    ps.setString(3,sales.getSalesTime());
    return ps;
}, keyHolder);

return keyHolder.getKey().longValue();

[!warning] DB 툴에서 특정 테이블에 대해 update를 수행하는 경우,
툴은 보통 Commit을 하기 전 까지 대상 테이블에 대해 Lock을 잡고 있게 된다.
그래서 애플리케이션에서 테스트를 돌리는데 ‘어디선가 Blocking이 걸리는데?’ 같은 상황이 벌어질 수 있음.

RowMapper 종류

  • org.springframework.jdbc.core.BeanPropertyRowMapper
    • snake_case 컬럼을 자동으로 camelCase 필드로 매핑해준다.
    • NoArgsConstructor를 필요로 하기 때문에 data class에는 동작하지 않는다.
  • org.springframework.jdbc.core.DataClassRowMapper
    • BeanPropertyRowMapper를 확장해서,
    • NoArgConstructor가 없는 java record나 kotlin data class 대상으로 동작한다.
  • org.springframework.data.jdbc.core.convert.EntityRowMapper
    • Spring Data JDBC 에서 제공

:param bind (SqlParameterSource)

다음 3가지 방법 사용 할 수 있다.

1
2
3
4
5
6
7
8
9
1.
BeanPropertySqlParameterSource(obj)

2.
MapSqlParameterSource()
    .addValue("param1", param1)

3.
mapOf("param1" to param1)

LocalDateTime 같은 타입에 대한 Converter를 자동으로 적용하려면

아래 2가지 방법 사용 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1.
objectMapper.convertValue(toMap)
// objectMapper에 Converter가 등록되어 있어야 한다. (보통은 Auto-config)

2.
// ObjectSqlParameterSource를 직접 정의하고 override해서 Convert 처리
class ObjectSqlParameterSource(
    val obj: Any
): BeanPropertySqlParameterSource(obj) {
    override fun getValue(paramName: String): Any? {
        return when (val value = super.getValue(paramName)) {
            is LocalDateTime -> LocalDateTimeToStringConverter.convert(value)
            is CustomLocalDate -> CustomLocalDateToStringConverter.convert(value)
            is CardNumber -> CardNumberToStringConverter.convert(value)
            is CurrencyCode -> CurrencyCodeToStringConverter.convert(value)
            else -> value
        }
    }
    
    /**
     * BeanPropertySqlParameterSource.getSqlType을 보면 
     * JavaType이 Date이면 SqlType을 Timestamp로 만들어버린다.
     * 따라서 반드시 override 필요함.
     */
    override fun getSqlType(paramName: String): Int {
        val sqlType = super.getSqlType(paramName)
        return if (sqlType == Types.TIMESTAMP) {
            Types.VARCHAR
        } else {
            sqlType
        }
    }
}

기타

참고

JPA entity와 RowMapper는 호환성이 좋지 않다.

  • BeanPropertyRowMapper에서 컬럼과 필드는 column_name as fieldName 형태로 SQL에서 매핑을 지정해주는 방식이다.
  • (단점) 컬럼명 변경 시 SQL 찾아다니면서 모두 바꿔주어야 한다. 필드명 변경도 마찬가지로 SQL 찾아다니면서 바꿔주어야 한다.
  • @Column 애너테이션이 붙어있다면, 애너테이션에 명시된 컬럼명으로 필드에 주입해주도록 하면 as 연결 안해도 되므로 필드명 변경이 SQL과 무관해져 유지보수하기 더 낫다.
  • 하지만 이 밖에도 @Converter 같은 여러 JPA 애너테이션이 존재하는데, 이들을 모두 처리해주는 RowMapper를 작성해야 의미가 있다.

[!info] 여러모로 RowMapper를 사용하는 JDBC와의 궁합은 JPA 보다 Spring Data JDBC 쪽이 더 좋은 것 같다.

This post is licensed under CC BY 4.0 by the author.