JdbcTemplate는 설정이 편리하며 템플릿 콜백 패턴을 사용하여 Jdbc를 직접 사용할 때 발생하는 대부분의 반복 작업의 문제를 해결해준다. 우리는 SQL을 작성하고, 파라미터를 정의하고, 응답 값(ResultSet)을 매핑하기만 하면 된다.(RowMapper이용)
JdbcTemplate의 생성자로 dataSource를 받는다. Spring이 기본으로 등록하는 dataSource는 커넥션 풀을 이용하는 HikariCP이다. 커넥션으로 하여금 statement를 준비하고 실행하며 트랜잭션을 다루기 위한 커넥션 동기화(Service계층에서 @Transactional이 필요하겠죠?)와 예외 발생시 알아서 스프링 예외 변환기를 실행해준다.
DB의 내용에 수정이 적용되는 C,U,D를 적용하기 위한 메서드이다.
String sql = "update item set item_name=?, price=?, quantity=? where
id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
첫번째 파라미터로 sql문을 전달하고 순서대로(정확히) 변수를 파라미터로 전달하면 ?에 바인딩되어 sql문이 적용된다.
update메서드의 반환값은 int이며 영향 받은 로우 수를 반환한다.
만약 save작업을 시도하고, DB 테이블의 키는 자동증가한다면, keyHolder를 이용하여 save이후의 해당 레코드의 key를 반환받을 수 있다.
@Override
public Item save(Item item) {
String sql = "insert into item (item_name, price, quantity) values (?, ?, ?)";
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;
}
여기서 문제점은 실수없이 순서대로 파라미터 바인딩 내용을 잘 코딩해야하며, key를 반환받는 과정도 까다롭다. 이러한 문제를 NamedParameterJdbcTemplate로 하여금 편리하게 수정 가능하다.
@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;
파라미터 바인딩 방식은 ?에서:변수이름으로 바뀌었으며 우리는 변수이름과 데이터베이스 컬럼명을 바인딩할 정보인 Map기반의 param을 구성해줘야 한다.
param을 구성하기 위해 SqlParameterSource를 사용한다. 실제로 이것은 인터페이스이며 구현체인 BeanPropertySqlParameterSource 혹은 MapSqlParameterSource를 사용한다. 전자는 사용하기 매우 편리하지만(객체를 바로 파라미터 바인딩 시켜주어 코드가 짧다.) 객체 이외에 다른 변수를 sql문에 추가하는 것에는 한계가 있다. 후자는 체인 형식으로 구성 가능하지만 코드가 약간 길어질 수 있다.
BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(item);
MapSqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
이렇게 param을 구성했다면
template.update(sql, param);
template.update(sql, rowMapper(), param);
위와 같이 사용이 가능해진다. rowMapper()또한 쉽게 바인딩해주는 클래스를 제공해준다.
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class);
// 관례의 불일치에 의해 BeanPropertyRowMapper는 이를 자동적으로 바꾸어 준다.
// 데이터베이스의 item_name -> setItemName을 문제없이 실행한다.
}
sql을 기반으로 하는 관계형 데이터베이스는 보통 스네이크 케이스로 컬럼명이 결정된다. 반면 자바는 카멜 케이스를 이용한다.
RowMapper는 쿼리적용후 반환된 결과 레코드를 객체로 바꿔주기 위함이다. 레코드에 나타난 컬럼명은 item_name이지만 자바에서 사용하는 필드 네임은 itemName일 것이 보통적이므로 BeanPropertyRowMapper는 이를 고려하여 자동으로 이 변환과정(snake case to camel case)를 적용해서 객체로 변환해준다. 이 일치가 중요한 이유는 객체에 필드를 적재하기 위해 setter가 사용되기 때문이다.
만약 자바 객체 필드 이름이 itemName이 아니라면(ex.itemObjectName), 우리는 sql문 자체를 다음과 같은 예로 수정해서 반영할 수 있다.
sql insert문에 대해 이를 직접 작성하지 않고도 insert문을 적용가능하게 하는 SimpleJdbcInsert가 필요하다. 생성자로는 dataSource를 준다.
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) {
BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
jdbcInsert.executeAndReturnKey(param) 을 사용해서 INSERT SQL을 실행하고, 생성된 키 값도 매우 편리하게 조회할 수 있다.
하나의 로우를 조회할 때는 queryForObject()를 사용할 수 있다.
첫번째 파라미터로는 sql문을, 두번째로 바인딩될 변수의 타입, 그 뒤에는 변수들을 명시하면 된다.
int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class,
"Joe");
만약 뒤의 변수들이 같은 타입으로 여러개라면 "Joe"뒤에 계속해서 변수들을 적어주면 되지만, 바인딩될 파라미터 변수가 여러개이면서 타입이 다르다면 파라미터 전달 형식을 sql, 변수 배열, RowMapper로 전달할 수 있다.
public Actor findActorByNameAndAge(String firstName, int age) {
String sql = "SELECT * FROM t_actor WHERE first_name = ? AND age = ?";
return jdbcTemplate.queryForObject(
sql,
new Object[]{firstName, age}, // 파라미터 배열
new ActorRowMapper() // 결과를 매핑할 RowMapper
);
}
목록 조회를 위해 List<객체 클래스 타입>으로 반환을 받도록 하여 query()메서드를 이용할 수 있다.
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);
}
JdbcTemplate이 가진 최대 단점은, 동적 쿼리 문제를 해결하지 못한다는 점이다. SQL을 직접 적어주어야 한다는 부분도 상대적으로 볼 때 JPA와 같은 기술 대비 더 어렵고 복잡한 부분이라고 볼 수 있다.
동적 쿼리 문제를 해결해주면서도 SQL또한 더 편리하게 작성할 수 있게 도와주는 기술이 MyBatis로 다음 챕터에서 다뤄보도록 하겠다.