- 스프링 db 2편(데이터 접근 활용 기술) 시작
- 스프링 db 2편(데이터 접근 활용 기술)(2) 스프링 jdbctemplate
- 스프링 db 2편(데이터 접근 활용 기술)(3) 테스트
- 스프링 db 2편(데이터 접근 활용 기술)(4) mybatis
- 스프링 db 2편(데이터 접근 활용 기술) jpa
- 스프링 db 2편(데이터 접근 활용 기술)(2) 스프링 데이터 jpa
- 스프링 db 2편(데이터 접근 활용 기술)(3) querydsl
- 스프링 db 2편(데이터 접근 활용 기술)(4) 활용 방안
- 스프링 db 2편(데이터 접근 활용 기술)(5) 스프링 트랜잭션
- 스프링 db 2편(데이터 접근 활용 기술) 스프링 트랜잭션 전파 1 기본
- 스프링 db 2편(데이터 접근 활용 기술) 스프링 트랜잭션 전파 2 활용
스프링 db 2편(데이터 접근 활용 기술)(2) 스프링 jdbctemplate
0. JdbcTemplate 소개와 설정
- SQL을 직접 사용하는 경우에 스프링이 제공하는 JdbcTemplate은 아주 좋은 선택지다. JdbcTemplate 은 JDBC를 매우 편리하게 사용할 수 있게 도와준다.
장점
설정의 편리함
- JdbcTemplate은 spring-jdbc 라이브러리에 포함되어 있는데, 이 라이브러리는 스프링으로 JDBC를 사용할 때 기본으로 사용되는 라이브러리이다. 그리고 별도의 복잡한 설정 없이 바로 사용할 수 있다.
반복 문제 해결
- JdbcTemplate은 템플릿 콜백 패턴을 사용해서, JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해준다.
- 개발자는 SQL을 작성하고, 전달할 파리미터를 정의하고, 응답 값을 매핑하기만 하면 된다.
- 우리가 생각할 수 있는 대부분의 반복 작업을 대신 처리해준다.
- 커넥션 획득
- statement 를 준비하고 실행
- 결과를 반복하도록 루프를 실행
- 커넥션 종료, statement , resultset 종료
- 트랜잭션 다루기 위한 커넥션 동기화
- 예외 발생시 스프링 예외 변환기 실행
단점
- 동적 SQL을 해결하기 어렵다.
설정
-
build.gradle 에 다음 설정 추가
-
//JdbcTemplate 추가 implementation 'org.springframework.boot:spring-boot-starter-jdbc' //H2 데이터베이스 추가 runtimeOnly 'com.h2database:h2'
-
-
진행하기 전에 먼저 H2 데이터베이스에 item 테이블을 생성해야 한다.
-
drop table if exists item CASCADE; create table item ( id bigint generated by default as identity, item_name varchar(10), price integer, quantity integer, primary key (id) );
-
1. JdbcTemplate 적용1 - 기본
JdbcTemplateItemRepositoryV1
-
package hello.itemservice.repository.jdbcTemplate; import hello.itemservice.domain.Item; import hello.itemservice.repository.ItemRepository; import hello.itemservice.repository.ItemSearchCond; import hello.itemservice.repository.ItemUpdateDto; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.sql.DataSource; import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.List; import java.util.Optional; @Slf4j public class JdbcTemplateItemRepositoryV1 implements ItemRepository { 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 에서 만들어주는 key 를 사용하기 위해 불러옴 KeyHolder keyHolder = new GeneratedKeyHolder(); template.update((connection) -> { //자동 증가 키 PreparedStatement preparedStatement = connection.prepareStatement(sql, new String[]{"id"}); preparedStatement.setString(1, item.getItemName()); preparedStatement.setInt(2, item.getPrice()); preparedStatement.setInt(3, item.getQuantity()); return preparedStatement; }, 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{ //결과가 null 이면 항상 예외가 터짐(EmptyResultDataAccessException) //따라서 ofNullable 이 아닌 of 메서드를 사용 Item item = template.queryForObject(sql, itemRowMapper(), id); 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 ((resultSet, rowNum) -> { Item item = new Item(); item.setId(resultSet.getLong("id")); item.setItemName(resultSet.getString("item_name")); item.setPrice(resultSet.getInt("price")); item.setQuantity(resultSet.getInt("quantity")); return item; }); } }
this.template = new JdbcTemplate(dataSource)
- JdbcTemplate 은 데이터소스( dataSource )가 필요하다.
- JdbcTemplateItemRepositoryV1() 생성자를 보면 dataSource 를 의존 관계 주입 받고 생성자 내부에서 JdbcTemplate 을 생성한다.
- 스프링에서는 JdbcTemplate 을 사용할 때 관례상 이 방법을 많이 사용한다. 물론 JdbcTemplate 을 스프링 빈으로 직접 등록하고 주입받아도 된다.
Save
-
@Override public Item save(Item item) { String sql = "insert into item(item_name, price, quantity) values (?,?,?)"; //db 에서 만들어주는 key 를 사용하기 위해 불러옴 KeyHolder keyHolder = new GeneratedKeyHolder(); template.update((connection) -> { //자동 증가 키 PreparedStatement preparedStatement = connection.prepareStatement(sql, new String[]{"id"}); preparedStatement.setString(1, item.getItemName()); preparedStatement.setInt(2, item.getPrice()); preparedStatement.setInt(3, item.getQuantity()); return preparedStatement; }, keyHolder); long key = keyHolder.getKey().longValue(); item.setId(key); return item; }
- template.update() : 데이터를 변경할 때는 update() 를 사용하면 된다.
- INSERT , UPDATE , DELETE SQL에 사용한다.
- template.update() 의 반환 값은 int 인데, 영향 받은 로우 수를 반환한다.\
- template.update() : 데이터를 변경할 때는 update() 를 사용하면 된다.
return 을 받기 위해 memberId 를 받아야 한다.
- 데이터를 저장할 때 PK 생성에 identity (auto increment) 방식을 사용하기 때문에, PK인 ID 값을 개발자가 직접 지정하는 것이 아니라 비워두고 저장해야 한다. 그러면 데이터베이스가 PK인 ID를 대신 생성해준다.
- 제는 이렇게 데이터베이스가 대신 생성해주는 PK ID 값은 데이터베이스가 생성하기 때문에, 데이터베이스에 INSERT가 완료 되어야 생성된 PK ID 값을 확인할 수 있다.
- KeyHolder 와 connection.prepareStatement(sql, new String[]{“id”}) 를 사용해서 id 를 지정해주면 INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID 값을 조회할 수 있다.
- 물론 데이터베이스에서 생성된 ID 값을 조회하는 것은 순수 JDBC로도 가능하지만, 코드가 훨씬 더 복잡하다.
- 참고로 뒤에서 설명하겠지만 JdbcTemplate이 제공하는 SimpleJdbcInsert 라는 훨씬 편리한 기능이 있으므로 대략 이렇게 사용한다 정도로만 알아두면 된다.
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); }
- template.update() : 데이터를 변경할 때는 update() 를 사용하면 된다.
- ”?” 에 바인딩할 파라미터를 순서대로 전달하면 된다.
- 반환 값은 해당 쿼리의 영향을 받은 로우 수 이다. 여기서는 where id=? 를 지정했기 때문에 영향 받은 로우수는 최대 1개이다.
- template.update() : 데이터를 변경할 때는 update() 를 사용하면 된다.
findById()
-
@Override public Optional<Item> findById(Long id) { String sql = "select id, item_name, price, quantity from item where id = ?"; try{ //결과가 null 이면 항상 예외가 터짐(EmptyResultDataAccessException) //따라서 ofNullable 이 아닌 of 메서드를 사용 Item item = template.queryForObject(sql, itemRowMapper(), id); return Optional.of(item); }catch (EmptyResultDataAccessException e){ return Optional.empty(); } }
-
template.queryForObject()
- 결과 로우가 하나일 때 사용한다.
- RowMapper 는 데이터베이스의 반환 결과인 ResultSet 을 객체로 변환한다.
- 결과가 없으면
EmptyResultDataAccessException
예외가 발생한다. - 결과가 둘 이상이면
IncorrectResultSizeDataAccessException
예외가 발생한다
queryForObject() 인터페이스 정의
-
<T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
findAll()
-
@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()); }
template.query()
- 결과가 하나 이상일 때 사용한다.
- RowMapper 는 데이터베이스의 반환 결과인 ResultSet 을 객체로 변환한다.
- 결과가 없으면 빈 컬렉션을 반환한다.
- 동적 쿼리에 대한 부분은 바로 다음에 다룬다.
query() 인터페이스 정의
-
<T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
itemRowMapper()
-
private RowMapper<Item> itemRowMapper() { return ((resultSet, rowNum) -> { Item item = new Item(); item.setId(resultSet.getLong("id")); item.setItemName(resultSet.getString("item_name")); item.setPrice(resultSet.getInt("price")); item.setQuantity(resultSet.getInt("quantity")); return item; }); }
-
데이터베이스의 조회 결과를 객체로 변환할 때 사용한다.
-
JDBC를 직접 사용할 때 ResultSet 를 사용했던 부분을 떠올리면 된다.
-
차이가 있다면 다음과 같이 JdbcTemplate이 다음과 같은 루프를 돌려주고, 개발자는 RowMapper 를 구현해서 그 내부 코드만 채운다고 이해하면 된다.
-
while(resultSet 이 끝날 때 까지) { rowMapper(rs, rowNum) }
-
2. JdbcTemplate 적용2 - 동적 쿼리 문제
- 결과를 검색하는 findAll() 에서 어려운 부분은 사용자가 검색하는 값에 따라서 실행하는 SQL이 동적으로 달려져야 한다는 점이다.
- 검색 조건이 없을 떄 :
select id, item_name, price, quantity from item
- 상품명( itemName )으로 검색 :
select id, item_name, price, quantity from item where item_name like concat('%',?,'%')
- 최대 가격( maxPrice )으로 검색 :
select id, item_name, price, quantity from item where price <= ?
- 둘 다 검색 :
select id, item_name, price, quantity from item where item_name like concat('%',?,'%') and price <= ?
- 결과적으로 4가지 상황에 따른 SQL을 동적으로 생성해야 하고, 떤 경우에는 where 를 앞에 넣고 어떤 경우에는 and 를 넣어야 하는지 등을 모두 계산해야 한다.
- 검색 조건이 없을 떄 :
즉 매우 복잡하고, 오류가 날 가능성이 높다는 말
3. JdbcTemplate 적용3 - 구성과 실행
- 일단 실제 코드가 동작하도록 구성하고 실행해보자
JdbcTemplateV1Config
-
JdbcTemplateItemRepositoryV1 를 사용하기 위해 dataSource 를 DI
-
package hello.itemservice.config; import javax.sql.DataSource; @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); } }
ItemServiceApplication - 변경
-
//@Import(MemoryConfig.class) @Import(JdbcTemplateV1Config.class) @SpringBootApplication(scanBasePackages = "hello.itemservice.web") public class ItemServiceApplication {}
데이터베이스 접근 설정
-
src/main/resources/application.properties
-
spring.profiles.active=local spring.datasource.url=jdbc:h2:tcp://localhost/~/test spring.datasource.username=sa
- 이렇게 설정만 하면 스프링 부트가 해당 설정을 사용해서 커넥션 풀과 DataSource , 트랜잭션 매니저를 스프링 빈으로 자동 등록한다.
로그 추가
-
dbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties 에 다음을 추가하면 된다. main , test 설정이 분리되어 있기 때문에 둘다 확인하려면 두 곳에 모두 추가해야 한다.
-
#jdbcTemplate sql log logging.level.org.springframework.jdbc=debug
Executing prepared SQL statement [select id, item_name, price, quantity from item]
처럼 출력된다.
4. JdbcTemplate - 이름 지정 파라미터 1(NamedParameterJdbcTemplate)
JdbcTemplate 문제점
-
JdbcTemplate을 기본으로 사용하면 파라미터를 순서대로 바인딩 한다.
-
String sql = "update item set item_name=?, price=?, quantity=? where id=?"; template.update(sql, itemName, price, quantity, itemId);
- 여기서는 itemName , price , quantity 가 SQL에 있는 ? 에 순서대로 바인딩 된다. 따라서 순서만 잘 지키면 문제가 될 것은 없다. 그런데 문제는 변경시점에 발생한다.
- 누군가가 sql문을
= "update item set item_name=?, quantity=?, price=? where id=?";
이렇게 변경한다면 심각한 문제가 발생한다.
개발을 할 때는 코드를 몇줄 줄이는 편리함도 중요하지만, 모호함을 제거해서 코드를 명확하게 만드는 것이 유지보수 관점에서 매우 중요하다.
이름 지정 바인딩(NamedParameterJdbcTemplate)
- JdbcTemplate은 이런 문제를 보완하기 위해
NamedParameterJdbcTemplate
라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공한다.
JdbcTemplateItemRepositoryV2
-
? 대신 :itemName 과 같이 사용한다.
-
package hello.itemservice.repository.jdbcTemplate; import ...; /** * NamedParameterJdbcTemplate 사용 * SqlParameterSource * - BeanPropertySqlParameterSource * - MapSqlParameterSource * Map * * BeanPropertyRowMapper * */ @Slf4j public class JdbcTemplateItemRepositoryV2 implements ItemRepository { private final NamedParameterJdbcTemplate template; //JdbcTemplateItemRepositoryV2 생성자를 보면 의존관계 주입은 dataSource 를 받고 내부에서 NamedParameterJdbcTemplate 을 생성해서 가지고 있다. 스프링에서는 JdbcTemplate 관련 기능을 사용할 때 관례상 이 방법을 많이 사용한다. public JdbcTemplateItemRepositoryV2(DataSource dataSource) { this.template = new NamedParameterJdbcTemplate(dataSource); } @Override public Item save(Item item) { //? 대신 :itemName 과 같이 사용한다. String sql = "insert into item(item_name, price, quantity) " + "values (:itemName, :price, :quantity)"; //Item 의 필드이름대로 parameter 를 넘긴다. 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"; //이렇게도 쓸 수 있다. 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{ //이렇게도 쓸 수 있다. 1개이므로 맵으로 만들어서 넘긴다. 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()); } private RowMapper<Item> itemRowMapper() { return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원 } }
5. JdbcTemplate - 이름 지정 파라미터 2
- 파라미터를 전달하려면 Map 처럼 key , value 데이터 구조를 만들어서 전달해야 한다.
- 여기서 key 는 ‘:파리이터이름’ 으로 지정한, 파라미터의 이름이고 , value 는 해당 파라미터의 값이 된다.
- 다음 코드를 보면 이렇게 만든 파라미터( param )를 전달하는 것을 확인할 수 있다.
template.update(sql, param, keyHolder);
이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 크게 3가지가 있다.
- Map
- SqlParameterSource
- MapSqlParameterSource
- BeanPropertySqlParameterSource
Map
-
단순히 Map 을 사용한다.
-
findById() 코드에서 확인할 수 있다.
-
Map<String, Object> param = Map.of("id", id); Item item = template.queryForObject(sql, param, itemRowMapper());
-
MapSqlParameterSource
-
Map 과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.
-
SqlParameterSource 인터페이스의 구현체이다.
-
MapSqlParameterSource 는 메서드 체인을 통해 편리한 사용법도 제공한다.
-
update() 코드에서 확인할 수 있다.
-
SqlParameterSource param = new MapSqlParameterSource() .addValue("itemName", updateParam.getItemName()) .addValue("price", updateParam.getPrice()) .addValue("quantity", updateParam.getQuantity()) .addValue("id", itemId); //이 부분이 별도로 필요하다. template.update(sql, param);
-
BeanPropertySqlParameterSource
- 자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
- 예) ( getXxx() -> xxx, getItemName() -> itemName )
- key=itemName, value=상품명 값
- key=price, value=가격 값
- 예) ( getXxx() -> xxx, getItemName() -> itemName )
- SqlParameterSource 인터페이스의 구현체이다.
- save() , findAll() 코드에서 확인할 수 있다.
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
- 여기서 보면 BeanPropertySqlParameterSource 가 많은 것을 자동화 해주기 때문에 가장 좋아보이지만, BeanPropertySqlParameterSource 를 항상 사용할 수 있는 것은 아니다.
- 예를 들어서 update() 에서는 SQL에 :id 를 바인딩 해야 하는데, update() 에서 사용하는 ItemUpdateDto 에는 itemId 가 없다. 따라서 BeanPropertySqlParameterSource 를 사용할 수 없고, 대신에 MapSqlParameterSource 를 사용했다.
BeanPropertyRowMapper
-
private RowMapper<Item> itemRowMapper() { return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원 }
-
BeanPropertyRowMapper 는 ResultSet 의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환한다. 예를 들어서 데이터베이스에서 조회한 결과가 select id, price 라고 하면 다음과 같은 코드를 작성해준다. (실제로는 리플렉션 같은 기능을 사용한다.)
-
Item item = new Item(); item.setId(rs.getLong("id")); item.setPrice(rs.getInt("price"));
-
이 때 snake_case 는 자동으로 해결되니 그냥 두면 되고, 컬럼 이름과 객체 이름이 완전히 다른 경우에는 조회 SQL에서 별칭을 사용하면 된다.
6. JdbcTemplate - 이름 지정 파라미터 3
- 이름 지정 파라미터를 사용하도록 구성하고 실행해보자.
JdbcTemplateV2Config
-
앞서 개발한 JdbcTemplateItemRepositoryV2 를 사용하도록 스프링 빈에 등록한다.
-
package hello.itemservice.config; import ...; import javax.sql.DataSource; @Configuration @RequiredArgsConstructor public class JdbcTemplateV2Config { private final DataSource dataSource; @Bean public ItemService itemService() { return new ItemServiceV1(itemRepository()); } @Bean public ItemRepository itemRepository() { return new JdbcTemplateItemRepositoryV2(dataSource); } }
ItemServiceApplication - 변경
- JdbcTemplateV2Config.class 를 설정으로 사용하도록 변경되었다.
- @Import(JdbcTemplateV1Config.class) -> @Import(JdbcTemplateV2Config.class)
-
package hello.itemservice; import ...; @Import(JdbcTemplateV2Config.class) @SpringBootApplication(scanBasePackages = "hello.itemservice.web") public class ItemServiceApplication { public static void main(String[] args) { SpringApplication.run(ItemServiceApplication.class, args); } @Bean @Profile("local") public TestDataInit testDataInit(ItemRepository itemRepository) { return new TestDataInit(itemRepository); } }
7. JdbcTemplate - SimpleJdbcInsert
- JdbcTemplate은 INSERT SQL를 직접 작성하지 않아도 되도록 SimpleJdbcInsert 라는 편리한 기능을 제공한다.
JdbcTemplateItemRepositoryV3
save 만 적용가능하다.
-
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"); // .usingColumns("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; }
-
SimpleJdbcInsert 를 주입받는다.
-
이때 table 이름, keyColume 등을 넘긴다.
-
withTableName
: 데이터를 저장할 테이블 명을 지정한다. -
usingGeneratedKeyColumns
: key 를 생성하는 PK 컬럼 명을 지정한다 -
this.jdbcInsert = new SimpleJdbcInsert(dataSource) .withTableName("item") .usingGeneratedKeyColumns("id");
-
-
Columns 도 넘길 수 있는데, 생략가능하다.
usingColumns
: INSERT SQL에 사용할 컬럼을 지정한다. 특정 값만 저장하고 싶을 때 사용한다. 생략할 수 있다.
-
Config, ItemServiceApplication 설정 변경
- V3 -> V4 로 변경
8. JdbcTemplate 기능 정리
주요 기능
- JdbcTemplate : 순서 기반 파라미터 바인딩을 지원한다.
- NamedParameterJdbcTemplate : 이름 기반 파라미터 바인딩을 지원한다. (권장)
- SimpleJdbcInsert : INSERT SQL을 편리하게 사용할 수 있다.
- SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출할 수 있다.
조회
단건 조회 - 숫자 조회
-
int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor",Integer.class);
- 하나의 로우를 조회할 때는 queryForObject() 를 사용하면 된다. 지금처럼 조회 대상이 객체가 아니라 단순 데이터 하나라면 타입을 Integer.class , String.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);
- 객체 하나를 조회한다. 결과를 객체로 매핑해야 하므로 RowMapper 를 사용해야 한다. 여기서는 람다를 사용했다.
목록 조회 - 객체
-
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; });
- 여러 로우를 조회할 때는 query() 를 사용하면 된다. 결과를 리스트로 반환한다.
- 결과를 객체로 매핑해야 하므로 RowMapper 를 사용해야 한다. 여기서는 람다를 사용했다.
목록 조회 - 객체(RowMapper 분리)
-
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> { Actor actor = new Actor(); actor.setFirstName(resultSet.getString("first_name")); actor.setLastName(resultSet.getString("last_name")); return actor; }; public List<Actor> findAllActors() { return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper); }
- 여러 로우를 조회할 때는 query() 를 사용하면 된다. 결과를 리스트로 반환한다.
- 여기서는 RowMapper 를 분리했다. 이렇게 하면 여러 곳에서 재사용 할 수 있다.
변경(INSERT, UPDATE, DELETE)
- 데이터를 변경할 때는 jdbcTemplate.update() 를 사용하면 된다. 참고로 int 반환값을 반환하는데, SQL 실행 결과에 영향받은 로우 수를 반환한다.
등록
-
jdbcTemplate.update( "insert into t_actor (first_name, last_name) values (?, ?)", "Leonor", "Watling");
수정
-
jdbcTemplate.update( "update t_actor set last_name = ? where id = ?", "Banjo", 5276L);
삭제
-
jdbcTemplate.update( "delete from t_actor where id = ?", Long.valueOf(actorId));
기타기능
- 임의의 SQL을 실행할 때는 execute() 를 사용하면 된다. 테이블을 생성하는 DDL에 사용할 수 있다.
DDL
-
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
스토어드 프로시저 호출
-
jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId));
그런데 JdbcTemplate의 최대 단점이 있는데, 바로 동적 쿼리 문제를 해결하지 못한다는 점이다. 그리고 SQL을 자바 코드로 작성하기 때문에 SQL 라인이 코드를 넘어갈 때 마다 문자 더하기를 해주어야 하는 단점도 있다. 동적 쿼리 문제를 해결하면서 동시에 SQL도 편리하게 작성할 수 있게 도와주는 기술이 바로 MyBatis 이다.
댓글남기기