김영한님의 스프링 DB 2편 을 공부하여 정리한 글입니다.
@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(?,?,?)";
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 {
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;
});
}
}
@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);
}
}
//@Import(MemoryConfig.class)
@Import(JdbcTemplateV1Config.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);
}
}
NamedParameterJdbcTemplate 라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공한다.
@Slf4j
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
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)";
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";
MapSqlParameterSource 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();
BeanPropertySqlParameterSource 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);
}
}
이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 크게 3가지가 있다.
Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
Map 과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.
MapSqlParameterSource 는 메서드 체인을 통해 편리한 사용법도 제공한다.
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId); //이 부분이 별도로 필요하다.
template.update(sql, param);
자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
예를 들어서 getItemName() , getPrice() 가 있으면 다음과 같은 데이터를 자동으로 만들어낸다.
key=itemName, value=상품명 값
key=price, value=가격 값
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
BeanPropertySqlParameterSource 가 많은 것을 자동화 해주기 때문에 가장 좋아보이지만,
BeanPropertySqlParameterSource 를 항상 사용할 수 있는 것은 아니다.
update() 에서는 SQL에 :id 를 바인딩 해야 하는데, update() 에서 사용하는 ItemUpdateDto 에는 itemId 가 없다. 따라서 BeanPropertySqlParameterSource 를 사용할 수 없고,
대신에 MapSqlParameterSource 를 사용했다.
select item_name 의 경우 setItem_name() 이라는 메서드가 없기 때문에 골치가 아프다.
이런 경우 개발자가 조회 SQL을 다음과 같이 고치면 된다.
select item_name as itemName
별칭 as 를 사용해서 SQL 조회 결과의 이름을 변경하는 것이다.
자바 객체는 카멜( camelCase ) 표기법을 사용한다.
반면에 관계형 데이터베이스에서는 주로 언더스코어를 사용하는 snake_case 표기법을 사용한다.
BeanPropertyRowMapper 는 언더스코어 표기법을 카멜로 자동 변환해준다.
따라서 select item_name 으로 조회해도 setItemName() 에 문제 없이 값이 들어간다.
컬럼 이름과 객체 이름이 완전히 다른 경우에는 조회 SQL에서 별칭을 사용하면 된다.
INSERT SQL를 직접 작성하지 않아도 되도록 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");
// .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;
}
}
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() 를 사용하면 된다. 결과를 리스트로 반환한다.
데이터를 변경할 때는 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에 사용할 수 있다.
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));