[Spring] JDBC Template
- -
JDBC Template
스프링이 제공하는 JdbcTemplate
은 JDBC를 편리하게 사용할 수 있게 도와준다.
JdbcTemplate
은 spring-jdbc
라이브러리에 포함되어 있어 별도의 복잡한 설정 없이 바로 사용할 수 있다.
또한 JdbcTemplate
은 템플릿 콜백 패턴을 사용하여 JDBC를 직접 사용할 때 발생하는 커넥션 획득, statement
준비 및 실행, 커넥션 및 statement
, resultset
종료, 트랜잭션을 위한 커넥션 동기화, 예외 변환 등 대부분의 반복 작업을 대신 처리해준다.JdbcTemplate
을 사용하면 개발자는 SQL 작성, 파라미터 전달, 응답값 매핑만 하면 된다.
의존성 설정
JdbcTemplate
을 사용하기 위해서는 의존성을 설정해줘야 한다.build.gradle
에 다음 내용을 추가해준다.
dependencies {
// JdbcTemplate
implementation ‘org.springframework.boot:spring-boot-starter-jdbc’
}
JdbcTemplate 기능
JdbcTemplate
는 대표적으로 다음과 같은 기능을 지원한다.
JdbcTemplate
: 순서 기반 파라미터 바인딩 지원NamedParameterJdbcTemplate
: 이름 기반 파라미터 바인딩 지원SimpleJdbcInsert
:insert
SQL을 편리하게 사용 가능SimpleJdbcCall
: 스토어드 프로시저 호출 가능
JdbcTemplate 사용
JdbcTemplate
은 순서 기반 파라미터 바인딩을 지원한다.JdbcTemplate
생성 시점에 DataSource
를 넘겨주어야 한다.
다음과 같이 사용할 수 있다.
public class JdbcTemplateItemRepository {
private final JdbcTemplate template;
public JdbcTemplateItemRepository(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
// insert
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;
}
// update
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
);
}
// select
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = ?";
try {
// queryForObject() : 결과가 없으면 예외 발생.
Item item = template.queryForObject(sql, itemRowMapper(), id);
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
// select
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());
}
// resultset to Object
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;
};
}
}
데이터를 조작하는 명령(INSERT, UPDATE, DELETE SQL)은 JdbcTemplate.update()
메서드를 사용한다.
반환값은 int
로 영향을 받은 행 수를 반환한다.save()
메서드에서 보듯이 자동 증가 컬럼이 있다면 해당 컬럼은 SQL 문에서 비워준다.insert
수행 이후 저장된 PK 값을 받기 위해 KeyHolder
와 con.prepareStatement(sql, new String[]{"id"})
를 통해 자동 증가된 컬럼을 지정해주면, insert
이후 자동 증가된 컬럼 값이 KeyHolder
에 담긴다.
파라미터 바인딩을 위해서는 JdbcTemplate
메서드의 args
부분에 ?
에 바인딩 할 파라미터를 순서대로 전달하여 사용하면 된다.
하지만 이는 파라미터를 순서대로 바인딩 하기 때문에 순서가 맞지 않아 오류가 발생할 경우가 생길 수 있다는 문제가 있다.
NamedParameterJdbcTemplate
JdbcTemplate
의 순서 기반 파라미터 바인딩의 문제를 보완하기 위해 NamedParameterJdbcTemplate
을 제공한다.
NamedParameterJdbcTemplate
도 역시 생성 시점에 DataSource
를 넘겨주어야 한다.
public class JdbcTemplateItemRepository {
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
// 빈 프로퍼티 기반 파라미터 매핑
public Item save(Item item) {
String sql = "insert into item (item_name, price, quantity) " +
"values (:itemName, :price, :quantity)";
// BeanPropertySqlParameterSource를 사용할 수 있다.
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
Long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
// 파라미터 이름 기반 매핑
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item " +
"set item_name=:itemName, price=:price, quantity=:quantity " +
"where id=:id";
// MapSqlParamterSource를 사용할 수 있다.
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
template.update(sql, param);
}
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = :id";
try {
// Map을 사용하여 sql 파라미터 이름에 바인딩할 수 있다.
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();
}
}
public List<Item> findAll(ItemSearchCond cond) {
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
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() {
// BeanPropertyRowMapper는 리플렉션 등의 기능을 이용하여 ResultSet의 결과를 통해 자바빈 규약에 맞추어 데이터를 변환한다.
// ResultSet 필드 이름과 변환 객체의 필드 이름이 다를 경우, sql에 as를 통해 별칭을 주어 해결한다.
return BeanPropertyRowMapper.newInstance(Item.class); // camelcase 지원
}
}
findById
메서드에서와 같이 단순히 Map
을 사용해서 NamedParameterJdbcTemplate
메서드에 넘겨서 이름 기반으로 파라미터 바인딩을 할 수 있다.
또한 update
메서드와 같이 MapSqlParameterSource
를 사용하고, 이를 NamedParameterJdbcTemplate
에 넘겨줄 수도 있다.MapSqlParameterSource
는 Map
과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.
save
메서드와 같이BeanPropertySqlParameterSource
를 사용하는 방법도 있다.BeanPropertySqlParameterSource
은 자바 빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성하는 방법이다.BeanPropertySqlParameterSource
생성 시점에 특정 객체를 인자로 받으면, 해당 객체의 프로퍼티를 통해 파라미터를 자동으로 만들어낸다. ex) getItemName() -> key : itemName, value : 'itemName 값'
SimpleJdbcInsert
SimpleJdbcInsert
는 insert
SQL을 직접 작성하지 않아도 되도록 돕는 기능이다.
SimpleJdbcInsert
생성 시점에 DataSource
와 데이터를 저장할 테이블 이름, key
를 생성하는 PK 컬럼, insert
SQL에 사용할 컬럼들의 이름을 지정한다.insert
SQL에 사용할 컬럼 이름들은 생략할 수 있으며, 특정 값만 저장하고 싶을 때 사용할 수 있다.
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"); // 생략 가능
}
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
}
SimpleJdbcCall
추가로 스토어드 프로시저를 사용할 수 있도록 SimpleJdbcCall
을 지원한다.
'framework > spring' 카테고리의 다른 글
[Spring] Logback 설정 (1) | 2023.02.05 |
---|---|
[Spring] Spring REST Docs, Swagger 조합. restdocs-api-spec (0) | 2023.01.08 |
[Spring] Web-Socket, SockJS, STOMP 이론 (0) | 2023.01.06 |
[Spring] 마이바티스(MyBatis) (0) | 2022.12.11 |
[Spring] 애플리케이션 실행시 DB 테이블 및 데이터 입력 (0) | 2022.12.10 |
소중한 공감 감사합니다