일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch2
- 스프링 mvc2 - 타임리프
- 스프링 db1 - 스프링과 문제 해결
- 2024 정보처리기사 수제비 실기
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch6
- jpa - 객체지향 쿼리 언어
- 게시글 목록 api
- 타임리프 - 기본기능
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch8
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch13
- @Aspect
- 2024 정보처리기사 시나공 필기
- 스프링 입문(무료)
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch9
- 스프링 mvc2 - 로그인 처리
- 스프링 mvc2 - 검증
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch14
- Today
- Total
나구리의 개발공부기록
스프링 JdbcTemplate 소개와 설정, JdbcTemplate 적용(기본/동적 쿼리 문제/구성과 실행), 이름지정 파라미터, SimpleJdbcInsert, 기능 정리 본문
스프링 JdbcTemplate 소개와 설정, JdbcTemplate 적용(기본/동적 쿼리 문제/구성과 실행), 이름지정 파라미터, SimpleJdbcInsert, 기능 정리
소소한나구리 2024. 9. 19. 14:11 출처 : 인프런 - 스프링 DB 2편 데이터 접근 핵심 원리 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
1. JdbcTemplate 소개와 설정
- SQL을 직접 사용하는 경우 스프링이 제공하는 JdbcTemplate는 아주 좋은 선택지이며 JDBC를 매우 편리하게 사용할 수 있게 도와줌
1) 장점
(1) 설정의 편리함
- 별도의 복잡한 설정없이 바로 사용할 수 있으며 JdbcTemplate는 spring-jdbc 라이브러리에 포함되어 있는데, 이 라이브러리는 스프링으로 JDBC를 사용할 때 기본으로 사용되는 라이브러리임
(2) 반복 문제 해결
- 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할 때 발생하는 대부분의 반복작업을 대신 처리해주어 개발자는 SQL을 작성하고 전달할 파라미터를 정의하고 응답 값을 매핑하기만 하면 됨
- 커넥션 획득, statement 준비 실행, 결과를 반복하도록 루프를 실행, 커넥션/statement/resultset 종료, 트랜잭션을 다루기 위한 커넥션 동기화, 예외 발생시 스프링 예외 변환기 실행 등의 생각할 수 있는 대부분의 반복작업을 대신 처리해 줌
2) 단점
동적 SQL을 해결하기 어려움
3) JdbcTemplate 설정
- build.gradle에 아래 2줄을 추가 후 코끼리 새로고침 클릭
- spring-jdbc 라이브러리를 추가하고, H2 데이터베이스에 접속해야 하기 때문에 H2 데이터베이스의 클라이언트 라이브러리를 함께 추가했음
//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'
** 진행 하기전에 꼭 H2 데이터베이스에 item 테이블을 생성해야 함 (DB에 값이 입력되어있다면 삭제 진행)
2. JdbcTemplate 적용1 - 기본
1) JdbcTemplateItemRepositoryV1
package hello.itemservice.repository.jdbctemplate;
/**
* JdbcTemplate
*/
@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {
// JdbcTemplate 준비
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity) values(?,?,?)";
// DB에서 identity 전략으로 값을 자동증가하도록 했기 때문에, 해당 값을 가져와서 저장하기위한 코드
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(connection -> {
// 자동 증가 키
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setString(1, item.getItemName());
ps.setInt(2, item.getPrice());
ps.setInt(3, item.getQuantity());
return ps;
}, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id=?";
try {
// queryForObject는 결과가 없으면 EmptyResultDataAccessException 예외가 터지므로 try - catch로 감싸기
Item item = template.queryForObject(sql, itemRowMapper(), id);
// .of(item) = item이 null 이면 안됨, .ofNullable(item) = item이 null이여도 됨
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty(); // 결과가 없을 때
}
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
// 보기 변하기 위해 검색 조건을 꺼냄
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
String sql = "select id, item_name, price, quantity from item";
// 동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
List<Object> param = new ArrayList<>();
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%', ?, '%')";
param.add(itemName);
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= ?";
param.add(maxPrice);
}
log.info("sql={}", sql);
return template.query(sql, itemRowMapper(), param.toArray());
}
private RowMapper<Item> itemRowMapper() {
return (rs, rowNum) -> {
Item item = new Item();
item.setId(rs.getLong("id"));
item.setItemName(rs.getString("item_name"));
item.setPrice(rs.getInt("price"));
item.setQuantity(rs.getInt("quantity"));
return item;
};
}
}
(1) 기본
- ItemRepository 인터페이스를 구현
- JdbcTemplate은 데이터소소스(dataSource)가 필요함
- JdbcTemplateItemRepositoryV1() 의 생성자를 보면 dataSource를 의존 관계 주입을 받고 생성자 내부에서 JdbcTemplate을 생성하는데 스프링에서 JdbcTemplate을 사용할 때 관례상 이 방법을 많이 사용하며, 스프링 빈으로 직접 등록하고 주입 받아도 됨
(2) save() - 데이터 저장
- template.update() : 데이터를 변경할 때 사용하며, INSERT, UPDATE, DELETE SQL에 사용되고 영향받은 row 갯수를 int로 반환홤
- 데이터를 저장할 때 PK 생성에 identity(auto increment)방식을 사용하기 때문에 PK인 ID 값을 개발자가 직접 지정하는 것이 아니라 비워두고 저장하면 데이터베이스가 PK인 ID를 대신 생성해줌
- 다만 PK인 ID값은 데이터베이스가 생성하기 때문에 데이터베이스에 INSERT가 완료 되어야 생성된 PK ID 값을 확인할 수 있는데, KeyHolder와 connection.prepareStatement(sql, new String[]{"id"})를 사용해서 id를 지정해주면 INSERT 쿼리 실행 이후에 DB에서 생성된 ID값을 조회할 수 있음
- 순수 JDBC로도 가능하지만 코드가 훨씬 복잡하며 JdbcTemplate이 제공하는 SimpleJdbcInsert라는 훨씬 편리한 기능이 있으므로 지금 상황에서는 이렇게 해야하는구나 정도로만 이해하면 됨
(3) update() - 데이터 수정
- template.update로 첫번째 파라미터는 sql을 전달하고, ?에 바인딩할 파라미터를 순서대로 전달
- 반환 값이 쿼리의 영향을 받은 row의 수이므로 where id=?를 지정했기 때문에 영향받은 로우 수는 0개이거나 1개임
(4) findById() - 데이터 하나 조회
- template.queryForObject()
- 결과 로우가 하나일 때 사용
- RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환함
- 결과가 없으면 EmptyResultDataAccessException 에외가 발생하고, 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생함
- 결과가 없을 때 Optional을 반환해야 하므로 결과가 없으면 예외를 잡아서 Optional.empty를 대신 반환
queryForObject() 인터페이스 정의
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args)
throws DataAccessException { }
(5) findAll()
- 데이터를 List로 조회하고 검색 조건으로 적절한 데이터를 찾음
- template.query()
- 결과가 하나일 때 사용하며 RowMapper는 DB의 반환 결과인 ResultSet을 객체로 변환함
- 결과가 없으면 빈 컬렉션을 반환함
- 동적 쿼리에 대한 부분은 밑에서 따로 설명
query() 인터페이스 정의
public <T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args)
throws DataAccessException { }
(6) itemRowMapper()
- DB의 조회 결과를 객체로 변환할 때 사용하는 메서드를 정의
- JDBC를 직접 사용할 때 ResultSet을 사용했던 부분과 동일한 부분인데, 차이는 JdbcTemplate이 resultSet이 끝날 때까지 while문으로 루프를 돌려주고 개발자는 RowMapper를 구현해서 그 내부 코드만 채운다고 보면 됨
3. JdbcTemplate 적용2 - 동적 쿼리 문제
- findAll() 에서 어려운 부분은 사용자가 검색하는 값에 따라서 실행하는 SQL이 동적으로 달라져야 한다는 점임
1) findAll()의 동적 쿼리 부분
/* 동적 쿼리 */
// itemName이나 maxPrice에 값이 있으면 sql 에 where 붙히기
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false; // true일때 sql에 and를 붙히기 위한 플래그 설정
List<Object> param = new ArrayList<>(); // 파라미터를 담을 ArrayList
// itemName의 결과가 존재하면 sql을 추가하고, param에 itemName의 값을 추가
// andFlag를 true로 변경 -> 다음 파라미터인 maxPrice를 받을 준비
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%', ?, '%')";
param.add(itemName);
andFlag = true;
}
// maxPrice에 값이 있을 때,
// andFlag가 true이면 sql에 and를 추가하고, false면 and를 추가하지 않음
// sql에 price <= ? 를 추가하고 param에 maxPrice의 값을 추가
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= ?";
param.add(maxPrice);
}
2) 동적 쿼리 조건
- 아래 4가지 상황에 따른 SQL을 동적으로 생성해야하는데, 동적 쿼리가 쉬워 보이지만 개발해보면 생각보다 다양한 상황을 고민해야 함
- 이렇게 간단한 경우에도 조건이 4가지나 되는데, 여기서 조건이 1개라도 더 추가된다고 하면 동적쿼리를 작성해야하는 코드는 더욱 복잡해질 것
- 어떤 경우에는 where를 앞에 넣고 어떤 경우에는 and를 넣어야 하는지 등등을 모두 계산하면서 코드를 작성해야하고, 각 상황에 맞추어 파라미터도 생성해줘야하며 실무에서는 이것보다 훨씬 더 복잡한 동적 쿼리들이 사용됨
(1) 검색 조건이 없을때
select id, item_name, price, quantity from item
(2) 상품명(itemName)으로 검색
select id, item_name, price, quantity from item where item_name like concat('%',?,'%')
(3) 최대 가격(maxPrice)으로 검색
select id, item_name, price, quantity from item where price <= ?
(4) 상품명, 최대가격 둘다 검색
select id, item_name, price, quantity from item
where item_name like concat('%',?,'%')
and price <= ?
** 이것이 JdbcTemplate의 가장 명확한 한계는 동적 쿼리를 개발자가 모든 상황을 고려해서 직접 코드를 짜야한다는 것인데, MyBatis를 사용하면 SQL을 직접 사용할 때 동적 쿼리를 쉽게 작성할 수 있음
4. JdbcTemplate 적용3 - 구성과 실행
- 실제 H2 DB에 코드가 동작하도록 구성 및 실행
1) JdbcTemplateV1Config
- ItemRepository 구현체로 JdbcTemplateItemRepositoryV1이 사용되도록 빈을 등록 -> 실제 DB에 연결하는 JdbcTemplate가 사용됨
package hello.itemservice.config;
@Configuration
@RequiredArgsConstructor
public class JdbcTemplateV1Config {
private final DataSource dataSource;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JdbcTemplateItemRepositoryV1(dataSource);
}
}
2) ItemServiceApplication 변경
- Import를 MemoryConfig -> JdbcTemplateV1Config로 변경하여 설정파일을 변경
//@Import(MemoryConfig.class)
@Import(JdbcTemplateV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
// ...
}
3) DB 접근 설정
- ~main/resources/application.properties에 설정을 추가하면 스프링부트가 해당 설저을 사요해서 커넥션 풀과 DataSource, 트랜잭션 매니저를 스프링 빈으로 자동으로 등록 -> 스프링 부트의 자동 리소스 등록 내용을 참고
- 패스워드가 없다면 주석처리된 부분을 아예 제거해도됨
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
#spring.datasource.password=
4) 실행
- 실행 후 테스트해보면 정상적으로 H2 DB에 데이터가 입력되고, 여러 기능 동작도 잘 작동됨
- 현재는 서버를 다시 시작할 때마다 TestDataInit이 실행되기 때문에 itemA, itemB가 데이터베이스에 계속 추가됨
5) 로그 추가
- JdbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties에 아래 설정을 추가하면 됨
- 지금은 main, test 프로필이 분리되어있기 때문에 확인하고자 하는 곳에 각각 추가해 주어야 함
#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug
5. JdbcTemplate - 이름 지정 파라미터 1
1) 기본 바인딩 - 순서대로
- JdbcTemplate를 기본으로 사용하면 SQL에 있는 ?에 파라미터를 순서대로 바인딩 하게 되는데 순서만 잘 지키면 문제가 될 것은 없지만 파라미터 순서를 변경하거나 할때 문제가 발생할 수 있음
- 아래 update 메서드가 있다고 할 때 누군가 SQL 코드의 순서를 변경했다고 가정해보면 quantity에 getPrice()의 값이 들어가고, price에 getQuantity()의 값이 들어가 버리게되는 매우 심각한 문제가 발생함
- 이런 일이 없을 것 같지만 실무에서는 파라미터가 10-20개가 넘는 일도 아주 많다고 하며, 미래에 필드를 추가하거나 수정하면서 문제가 충분히 발생할 수 있다고 함ㅁ
- 버그 중에서 가장 고치기 힘든 버그는 DB에 데이터가 잘못 들어가는 버그
- 애플리케이션 버그는 코드만 고치면 되지만 이것은 코드만 고치는 수준이 아니라 DB의 데이터를 복구해야 하기 때문에 버그를 해결하는데 들어가는 리소스가 어마어마하며, 이런 문제로 장애를 내어 퇴근하지 못하는 일이 발생함
- 개발을 할 때는 코드를 몇줄 줄이는 편리함도 중요하지만 모호함을 제거해서 코드를 명확하게 만드는 것이 유지 보수 관점에서 매우 중요하기 때문에, 파라미터를 순서대로 바인딩 하는 것은 편리하기는 해도 순서가 맞지 않아서 버그가 발생할 수도 있으므로 깊은 주의가 필요함
// 기존의 update 메서드
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
}
// 모종의 이유로 sql의 quantity와 price의 위치가 변경된 update 메서드
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name=?, quantity=?, price=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
}
2) 이름 지정 바인딩
- JdbcTemplate은 이런 문제를 보완하기 위해서 NamedParameterJdbcTemplate이라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공함
(1) JdbcTemplateItemRepositoryV2 추가
- JdbcTemplate이 아닌파라미터 이름으로 바인딩을 받는 NamedParameterJadbcTemplate를 사용
- 각 메서드의 SQL을 보면 ? 대신에 :파라미터이름을 받고 있으며, NamedParameterJdbcTemplate은 데이터베이스가 생성해주느 키를 매우 쉽게 조회하는 기능도 제공하여 데이터베이스가 생성해준 키를 조회하기 위해 길게 작성했던 코드가 제거됨
- 크게 3가지의 방법으로 파라미터 이름으로 바인딩 입력이 가능 -> 다음장에서 상세히 설명
package hello.itemservice.repository.jdbctemplate;
/**
* NamedParameterJdbcTemplate
* SqlParameterSource
* - BeanPropertySqlParameterSource
* - MapSqlParameterSource
* Map
*
* BeanPropertyRowMapper
*/
@Slf4j
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
// 파라미터 바인딩을 이름 기반으로하는 NamedParameterJdbcTemplate으로 변경
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity)" +
" values(:itemName, :price, :quantity)";
// 이름으로 바인딩하는 1번째 방법 - BeanProPertySEqlParameterSource()사용
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name=:itemName, price=:price, quantity=:quantity where id=:id";
// 이름으로 바인딩하는 2번째 방법 - MapSqlParameterSource()사용
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
template.update(sql, param);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id=:id";
try {
// 이름으로 바인딩하는 3번째 방법 - Map으로 작성 .of는 put대신 Map에 데이터를 입력하는 문법(java9 이상)
Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
SqlParameterSource param = new BeanPropertySqlParameterSource(cond);
String sql = "select id, item_name, price, quantity from item";
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%', :itemName, '%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= :maxPrice";
}
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());
}
// 람다식으로 작성했던 Mapper 코드를 BeanPropertyRowMapper.newInstance()로 해결
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); // camel변환 지원
}
}
6. JdbcTemplate - 이름 지정 파라미터 2
1) 이름 지정 파라미터
- 파라미터를 전달하려면 Map처럼 key, value 데이터 구조를 만들어서 전달해야 함
- key는 :파라미터이름 으로 지정한 파라미터의 이름, value는 해당 파라미터의 값
- template.update(sql, param, keyHolder); 처럼 만든 파라미터(param)를 전달함
(1) 이름 지정 바인딩에서 자주 사용하는 파라미터의 종류 3가지
- Map
- SqlParameterSource(인터페이스)
- MapSqlParameterSource
- BeanPropertySqlParameterSource
2) Map
- 단순히 Map을 사용해서 파라미터를 전달
// findById() 메서드 일부
Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
3) MapSqlParameterSource
- Map과 유사하며, SQL 타입을 지정할 수 있는 등 SQL에 더 특화된 기능을 제공
- SqlParameterSource 인터페이스의 구현체이며, 메서드 체인을 통해 파라미터를 입력하는 편리한 사용법도 제공함
// update() 메서드 일부
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId); // ItemUpdateDto에 없어서 별도로 필요함
template.update(sql, param);
4) BeanPropertySqlParameterSource
- 자바빈 프로퍼티 규약을 통해서 [getXxx() -> xxx, getItemName() -> itemName] 처럼 자동으로 파라미터 객체를 생성함
- 만일 getItemName(), getPrice()가 있다면 key=itemName, value=상품명, key=price, value=가격 데이터를 자동으로 만듦
- SqlParameterSource 인터페이스의 구현체
- BeanPropertySqlParameterSource가 많은 것을 자동화 해주기 때문에 가장 좋아보이지만 항상 사용할 수 있지는 못함
- 3)에서 예시를 든 update()를 보면 ItemUpdateDto에는 itemId가 없는데, update()에서는 SQL에 :id를 바인딩해야 하므로 BeanPropertySqlParameterSource를 통해 자동으로 데이터를 생성할 수 없어서 MapSqlParameterSource를 사용함
// save() 메서드 일부
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
// findAll() 메서드 일부
SqlParameterSource param = new BeanPropertySqlParameterSource(cond);
// ...
sql += " item_name like concat('%', :itemName, '%')";
// ...
sql += " price <= :maxPrice";
return template.query(sql, param, itemRowMapper());
5) BeanPropertyRowMapper
// V2
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); // camel변환 지원
}
// V1
private RowMapper<Item> itemRowMapper() {
return (rs, rowNum) -> {
Item item = new Item();
item.setId(rs.getLong("id"));
item.setItemName(rs.getString("item_name"));
item.setPrice(rs.getInt("price"));
item.setQuantity(rs.getInt("quantity"));
return item;
};
}
- BeanPropertyRowMapper를 사용하면 ResultSet의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환하여 DB에서 조회한 결과가 만약 select id, price라고 하면 리플렉션 같은 기능을 사용해서 아래와 같은 코드를 작성을 함
Item item = new Item();
item.setId(rs.getLong("id"));
item.setPrice(rs.getInt("price"));
- DB에서 조회한 결과 이름을 기반으로 V1 코드에서 우리가 직접 람다식으로 mapper()를 구현했던 것과 같이 setId(), setPrice() 처럼 자바빈 프로퍼티 규약에 맞춘 메서드를 호출함
(1) 별칭
- 만약 DB의 속성이 item_name의 경우 setItem_name()으로 메서드를 호출하게 되는데 개발자가 조회 SQL을 as(별칭)을 사용해서 select item_name as itemName 으로 작성하게 되면 해결됨
- 이런 방법을 실무에서도 자주 사용하며, 특히 데이터베이스 컬럼 이름과 객체 이름이 완전히 다를때 문제를 해결하는 용도로 자주 사용됨
- DB에는 member_name이라고 되어있고 객체에는 username 이라고 되어있을때 select member_name as username 으로 쉽게 해결할 수 있으며 JdbcTemplate은 물론 MyBatis 같은 기술에서도 자주 사용됨
(2) 관례의 불일치
- 실무에서 거의 95%는 자바 객체는 카멜표기법(camelCase)을 사용하고 관계형 데이터베이스에서는 대소문자를 구분하지 않는 특성 때문에 언더스코어를 사용하는 스네이크 표기법(snake_case)을 사용하고 있음
- 이렇게 관례적으로 많이 사용하다보니 BeanPropertyRowMapper는 스네이크 표기법을 카멜로 자동변환해주기 때문에 select item_name으로 조회해도 setItemName()에 문제 없이 값이 들어감
- BeanPropertyRowMapper를 사용하면 스네이크 표기법은 자동으로 해결되니, 컬럼 이름과 객체 이름이 완전히 다른 경우에만 조회 SQL에서 별칭을 사용해 문제를 해결하면 됨
7. JdbcTemplate - 이름 지정 파라미터 3
- 이름 지정 파라미터를 구성 및 실행
- 아래 설정을 변경하고 실행하면 NamedParameterJdbcTemplate을 사용하여 바인딩 순서에 관계없이 파라미터를 등록하게 되며 기능도 문제없이 동작함
1) JdbcTemplateV2Config 생성
- 나머지는 V1과 동일하고 리포지토리만 JdbcTemplateItemRepositoryV2로 변경
package hello.itemservice.config;
@Configuration
@RequiredArgsConstructor
public class JdbcTemplateV2Config {
//...
@Bean
public ItemRepository itemRepository() {
return new JdbcTemplateItemRepositoryV2(dataSource);
}
}
2) ItemServiceApplication 수정
- Import(JdbcTemplateV2Config.class)로 변경
@Import(JdbcTemplateV2Config.class)
8. SimpleJdbcInsert - Insert 편의기능
- JdbcTemplate은 Insert SQL을 직접 작성하지 않아도 되도록 SimpleJdbcInsert라는 기능을 제공함
1) JdbcTemplateItemRepositoryV3
- SimpleJdbcInsert를 사용하기 위해 생성자로 테이블명, 컬럼명 등을 지정하기 때문에 스프링 빈으로 등록하지 말고 직접 생성자로 입력하는 방식을 추천함
- 테이블명과, key를 생성하는 PK 컬럼명은 지정을 해주어야하며 INSERT SQL에 사용할 컬럼을 지정하는 코드는 생략이 가능하며 특정 값만 저장하고 싶을 때에는 생략 없이 해당 컬럼을 지정해주면 됨
- SimpleJdbcInsert는 생성 시점에 데이터베이스 테이블의 메타 데이터를 조회하기 때문에 어떤 컬럼이 있는지 확인할 수 있으므로 dataSource에 있는 컬럼을 전체 입력할 때는 생략이 가능함
- jdbcInsert.executeAndReturnKey(param)을 사용해서 Insert SQL을 실행하고, 생성된 키 값도 편리하게 조회가 가능함
- insert 쿼리를 할때만 사용할 수 있으므로 save() 메서드에만 적용되었으며 나머지는 동일함
package hello.itemservice.repository.jdbctemplate;
/**
* SimpleJdbcInsert
*/
@Slf4j
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
private final SimpleJdbcInsert jdbcInsert; // 추가
public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("item") // 테이블명
.usingGeneratedKeyColumns("id"); // pk컬럼명
// .userColumns("item_name", "price", "quantity"): // 생략 가능
}
@Override
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
// ... 이하 코드 동일
}
2) 실행
- JdbcTemplateV3Config를 생성후 리포지토리를 V3로 변경 (기존 생성 방식 참고)
- ItemServiceApplication의 Import를 JdbcTemplateV3Config로 변경
9. JdbcTemplate 기능 정리
1) JdbcTemplate 주요 기능
- JdbcTemplate : 순서 기반 파라미터 바인딩을 지원(간단한거 할때 사용)
- NamedParameterJdbcTemplate: 이름 기반 파라미터 바인딩을 지원(권장)
- SimpleJdbcInsert : INSERT SQL을 편리하게 사용 가능
- SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출 가능
** 참고
2) JdbcTemplate이 제공하는 여러가지 기능들
(1) 조회
- 하나의 로우를 조회할 때에는 qeuryForObject()를 사용하고 조회대상이 객체가 아니라 단순 데이터 하나라면 타입을 Integer.class, String.class와 같이 지정해주면됨
- 여러 로우를 조회할 때는 query()를 사용하고 결과를 리스트로 반환함
- 결과를 객체로 매핑해야 할때는 RowMapper를 사용해야 하는데, 람다를 사용해도되고, RowMapper를 별도의 메서드로 분리해도 되는데, 분리를 하면 여러 곳에서 재사용할 수 있음
// 단건 조회 - 숫자 조회
int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
// 단건 조회 - 숫자 조회, 파라미터 바인딩
int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class,"Joe");
// 단건 조회 - 문자 조회
String lastName = jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?", String.class, 1212L);
// 단건 조회 - 객체 조회(람다사용)
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
},
1212L);
// 목록 조회 - 객체(람다사용)
List<Actor> actors = jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
// 목록 조회 - 객체(RowMapper분리)
public List<Actor> findAllActors() {
return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}
(2)변경(INSERT, UPDATE, DELETE)
- 데이터 변경시에는 .update()를 사용하면 되며 SQL 실행 결과에 영향을 받은 로우의 수를 int 타입으로 반환함
// 등록 - insert
jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)", "Leonor", "Watling");
// 수정 - update
jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?", "Banjo", 5276L);
// 삭제 - delete
jdbcTemplate.update(
"delete from t_actor where id = ?", Long.valueOf(actorId));
(3) 기타 기능
- 임의의 SQL을 실행할때는 execute를 사용하면 됨(테이블 생성하는 DDL 등에 사용가능)
- 프로시저 호출 예시
// DDL
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
// 스토어드 프로시저 호출
jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId));