JdbcTemplate이란?
JdbcTemplate이란 SQL Mapper 기술 중 한 종류로 반복적인 Jdbc 코드를 대신 처리해주고 데이터베이스 테이블과 객체를 매핑하는 편리한 방법을 제공해준다.
따라서 개발자 입장에서는 SQL 쿼리를 작성하고 파라미터를 정의하고 응답값을 객체에 매핑만하면 된다.
이제 본격적인 사용법을 알아보자.
JdbcTemplate 사용하기
사용법을 이해하기 위한 테스트케이스는 다음과 같다.
1. 아이템 도메인(Item)
@Data
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
2. 아이템 리포지토리(ItemRepository)
public interface ItemRepository {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond cond);
}
- Item save(Item item): 아이템 객체를 데이터베이스에 저장
- void update(Long itemId, ItemUpdateDto updateParam): 아이템 객체를 id를 통해 업데이트
- Optional<Item> findById(Long id): id를 통해 아이템 객체 찾기
- List<Item> findAll(ItemSearchCond cond): 특정 조건의 Item 모두찾기
3. 아이템 검색 조건(ItemSearchCond)
@Data
public class ItemSearchCond {
private String itemName;
private Integer maxPrice;
public ItemSearchCond() {
}
public ItemSearchCond(String itemName, Integer maxPrice) {
this.itemName = itemName;
this.maxPrice = maxPrice;
}
}
설정하기
1) build.gradle
//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
JdbcTemplate 코드 ver1. - 기본
0. 전체 코드
/**
* JdbcTemplate
*/
@Slf4j
@Repository
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 (?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(con -> {
PreparedStatement ps = con.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{
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 ((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. JdbcTemplate
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
- JdbcTemplate 클래스는 인자로 Datasource를 주입받아야 한다.
2. Item save(Item item): 아이템 객체를 데이터베이스에 저장
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity) values (?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(con -> {
PreparedStatement ps = con.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;
}
- template.update()
- 데이터베이스의 데이터를 변경할 때 사용한다.
- INSERT, UPDATE, DELETE 쿼리에서 사용
- KeyHolder
- 기본키가 AUTO_INCREMENT방식으로 생성될 때 그 생성된 기본키를 잡아둘 때 사용한다.
- AUTO_INCREMENT방식에선 기본키가 데이터베이스에 의해 생성되기 때문에 INSERT 쿼리 실행 이후 기본키가 생성된다.
3. void update(Long itemId, ItemUpdateDto updateParam): 아이템 객체를 id를 통해 업데이트
@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()
- ?에 바인딩할 파라미터 순서대로 인자에 넣어주면 된다.
4. Optional<Item> findById(Long id): id를 통해 아이템 객체 찾기
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = ?"; // 결과 로우가 하나일 때 사용
try{
Item item = template.queryForObject(sql, itemRowMapper(), id);
return Optional.of(item);
} catch (EmptyResultDataAccessException e) { // 결과가 없으면 예외 발생
return Optional.empty();
}
}
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;
});
}
- template.queryForObject()
- 결과로우가 하나일 때 사용한다.
- 결과가 없으면 EmptyResultDataAccessException이 발생한다.
- 결과가 둘 이상이면 IncorrectResultSizeDataAccessException이 발생한다.
- RowMapper
- 테이블을 객체로 매핑해준다.
- 데이터베이스의 반환 결과인 ResultSet을 객체로 변환
5. List<Item> findAll(ItemSearchCond cond): 특정 조건의 Item 모두찾기
@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;
});
}
- template.query()
- 결과 로우가 여러 개일 때 사용한다.
- 결과 로우들을 객체형태로 리스트에 담아 반환한다.
- RowMapper
- 테이블을 객체로 매핑해준다.
- 데이터베이스의 반환 결과인 ResultSet을 객체로 변환
- 동적 쿼리
1. 검색 조건이 없는 경우
select id, item_name, price, quantity from item
2. 검색 조건이 상품명만 있는 경우
select id, item_name, price, quantity from item
where item_name like concat('%',?,'%')
3. 검색 조건이 가격만 있는 경우
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 <= ?
케이스에 맞게 동적으로 쿼리가 작성되게끔 코드를 작성해야한다.
이러한 동적쿼리 코드는 상당히 복잡하다. 다음 포스트에서 소개할 Mybatis를 사용하면 어느정도 이러한 부분을 해소할 수 있다.
JdbcTemplate 코드 ver2. - 이름 지정 파라미터
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql, itemName, price, quantity, itemId);
기존에는 위와 같이 ? 순서대로 파라미터가 바인딩 되었다.
하지만 다음과 같이 sql의 파라미터 순서가 변경된다면?
String sql = "update item set item_name=?, quantity=?, price=? where id=?";
template.update(sql, itemName, price, quantity,itemId);
이렇게 되면 파라미터가 다음과 같이 바인딩 된다.
item_name = itemName, quantity = price, price = quantity, id = itemId
즉, 데이터 불일치가 발생하게 된다.
이러한 문제를 방지하기 위해 JdbcTemplate에서는 NamedParameterJdbcTemplate이라는 이름지정 파라미터 기능을 제공한다.
코드를 보도록하자.
0. 전체 코드
/**
* NamedParameterJdbcTemplate
* SqlParameterSource
* - BeanPropertySqlParameterSource
* - MapSqlParameterSource
* Map
*
* BeanPropertyRowMapper
*
*/
@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
// private final JdbcTemplate template;
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)";
BeanPropertySqlParameterSource 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 {
Map<String, Long> 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);
}
}
1. NamedParameterJdbcTemplate
// private final JdbcTemplate template;
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
- NamedParameterJdbcTemplate도 datasource를 인자로 주입받는다.
2. 쿼리
String sql = "insert into item(item_name, price, quantity) " +
"values (:itemName, :price, :quantity)";
- 기존에는 쿼리를 작성할 때 파라미터를 ?로 작성하였다면 NamedParameterJdbcTemplate에서는 :파라미터 이름의 형태로 넣는다. .(ex :itemName)
3. 이름지정 파라미터를 사용하는 3가지 방법
1️⃣ Map
key를 파라미터의 이름, value를 파라미터의 값으로 하여 Map에 저장을 하여 넘겨준다.
예시: findById()
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = :id"; // 결과 로우가 하나일 때 사용
try {
Map<String, Long> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
return Optional.of(item);
} catch (EmptyResultDataAccessException e) { // 결과가 없으면 예외 발생
return Optional.empty();
}
}
2️⃣ MapSqlParameterSource
SqlParameterSource의 구현체로 Map과 크게 다른 기능은 없지만 SQL에 조금 더 특화된 기능을 제공.
메서드 체인기능을 통해 더 깔끔하게 파라미터를 저장할 수 있음
예시: update()
@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);
}
3️⃣ BeanPropertySqlParameterSource
SqlParameterSource의 구현체로 자바빈 프로퍼티 규약을 통해 파라미터 객체를 생성.
예: getXxx --> xxx, getItemName --> itemName
파라미터가 객체단위로 이루어져있을 때 사용하기 좋음
예시: save()
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity) " +
"values (:itemName, :price, :quantity)";
BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
어떤 상황에 어떤걸 사용해야할까?
(1) 파라미터가 객체로 이루어져있을 때 - BeanPropertySqlParameterSource
(2) 파라미터가 객체로 이루어지지 않을 때 - Map / MapSqlParameterSource
4. BeanPropertyRowMapper
ver1. 에서는 RowMapper를 통해 객체와 테이블을 매핑했다면
ver2. 에서는 BeanPropertyRowMapper를 사용했다.
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class);
}
BeanPropertyRowMapper는 ResultSet을 객체로 매핑할 때, 자바빈 프로퍼티 규약을 기반으로 매핑한다.
예를 들어, select id, price라고 하면, 다음과 같은 코드를 작성해준다.
Item item = new Item();
item.setId(rs.getId());
item.setPrice(rs.getPrice());
id를 보고 setId를, price를 보고 setPrice를 호출하는 것이다.
BeanPropertyRowMapper는 객체와 테이블의 네이밍 관례의 불일치까지 해결해준다.
객체의 경우 카멜 표기법(camelCase)을 사용한다. ex: itemName
테이블의 경우 스네이크 표기법(snake_case)을 사용한다. ex: item_name
즉, 네이밍이 불일치하기 때문에 다음과 같은 쿼리를 실행시 객체로 매핑할 때, 문제가 발생한다.
String sql = "select item_name from item";
item_name을 보고 setItem_name를 호출해야하는데 실제로는 setItemName이 존재한다.
BeanPropertyRowMapper는 이러한 불일치를 자동으로 변환시켜준다.
즉, item_name을 보고 setItem_name을 호출하는 것이 아니라 setItemName을 호출한다.
JdbcTemplate 코드 ver3. - SimpleJdbcInsert
SimpleJdbcInsert는 JdbcTemplate에서 INSERT관련 부분을 편리하게 처리할 수 있는 방법을 제공한다.
0. 전체코드
/**
* SimpleJdbcTemplate
*
*/
@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {
// private final JdbcTemplate template;
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;
}
@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 {
Map<String, Long> 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);
}
}
1. SimpleJdbcInsert
// private final JdbcTemplate template;
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"); // 생략 가능
}
- SimpleJdbcInsert는 dataSource를 인자로 주입해줘야 한다.
- withTableName(): 테이블의 이름
- usingGeneratedKeyColumns(): 테이블에서 사용하는 기본키 속성
- usingColumns(): 테이블에서 사용하는 속성들 (생략 가능)
정리
1. JdbcTemplate: 기본
- 데이터 변경(INSERT, UPDATE, DELETE): template.update()
- 데이터 단건조회: template.queryForObject()
- 데이터 다량조회: template.query()
2. NamedParameterJdbcTemplate: JdbcTemplate에서 쿼리의 파라미터 순서 변경시 발생하는 문제 해결
- 이름지정 파라미터 세가지
1) Map
2) MapSqlParameterSource
3) BeanPropertySqlParameterSource
- 데이터 변경 및 조회는 JdbcTemplate과 동일
3. SimpleJdbcInsert: INSERT쉽게 할 수 있음
4. 테이블 - 객체 매핑
- RowMapper: ResultSet을 직접 일일이 코드로 매핑
- BeanProperyRowMapper: ResultSet을 빈프로퍼티 규약을 통해 자동으로 매핑
5. JdbcTemplate 단점: 동적 쿼리 작성 복잡함
- Mybatis로 해결
해당 포스트는 김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 기반으로 작성되었습니다.
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
'Spring > Spring Data' 카테고리의 다른 글
[Spring Data] JPA (0) | 2024.06.07 |
---|---|
[Spring Data] MyBatis (0) | 2024.06.06 |
[Spring Data] SQL Mapper VS ORM (0) | 2024.06.05 |
[Spring Data] 예외로 인한 의존성과 스프링의 예외 추상화 (0) | 2024.04.30 |
[Spring Data] Spring Transaction을 통한 문제해결 (0) | 2024.04.29 |