(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();
- 근데 오라클에서는 포기하는게 좋다. 계속 삽질하다 결국 jdbcTemplate에서 지원 안한다 는 것을 알아냄.
- 그래서 오라클에서는 이렇게 처리…
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
}
}
}
기타
참고
- spring-jdbc-tips/spring-jdbc-core.md at master · benelog/spring-jdbc-tips
- [Spring JDBC] Spring JDBC를 이용한 데이터 접근 방식
- [Spring JDBC] JdbcTemplate의 기본 사용법
JPA entity와 RowMapper는 호환성이 좋지 않다.
BeanPropertyRowMapper
에서 컬럼과 필드는column_name as fieldName
형태로 SQL에서 매핑을 지정해주는 방식이다.- (단점) 컬럼명 변경 시 SQL 찾아다니면서 모두 바꿔주어야 한다. 필드명 변경도 마찬가지로 SQL 찾아다니면서 바꿔주어야 한다.
@Column
애너테이션이 붙어있다면, 애너테이션에 명시된 컬럼명으로 필드에 주입해주도록 하면as
연결 안해도 되므로 필드명 변경이 SQL과 무관해져 유지보수하기 더 낫다.- 하지만 이 밖에도
@Converter
같은 여러 JPA 애너테이션이 존재하는데, 이들을 모두 처리해주는 RowMapper를 작성해야 의미가 있다.
여러모로
RowMapper
를 사용하는 JDBC와의 궁합은 JPA 보다 Spring Data JDBC 쪽이 더 좋은 것 같다.
This post is licensed under CC BY 4.0 by the author.