이전에 메모리에다가 인스턴스를 올릴때는
@Repository
public class MemoryItemRepository implements ItemRepository {
private static final Map<Long, Item> store = new HashMap<>(); //static
private static long sequence = 0L; //static
@Override
public Item save(Item item) {
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
이런식으로 Map을 사용했다.
그러나 이제 DB로 H2를 사용하고 -> 해당 DB에 접근하기 위해서 JDBC 기술을 사용할 것이다.
해당 내용은 이 글을 참고 DB
이제 H2를 사용해서 JDBC 기술로 접근할 것이다.
itemRepository에 있는 save update findById findAll메서드를 이제는 JDBC 기술로 구현해보는것이다.
메모리,DB 방식으로 구현할수있게 -> itemRepository는 인터페이스로 설정했음
itemRepository
JdbcTemplateItemRepositoryV1
@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(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;
};
}
}
JDBC 기술의 자세한 내용을 여기선 알필요는 없다.
그러나 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 <= ?
상품명(itemName),최대가격(maxPrice)둘다 검색
select id, item_name, price, quantity from item
where item_name like concat('%',?,'%')
and price <= ?
각각의 경우마다 sql을 동적으로 다르게 만들어야한다.
개발자가 if문으로 조건을 만들다가 오류를 낼 가능성이 매우 높다.
Jdbc가 아닌 뒤에 설명할 MyBatis를 사용하면 동적쿼리를 쉽게 만들수 있다.
동적쿼리를 작성할때도 문제가 있다.
String sql = "update item set item_name=?,price=?,quantity=? where id = ?"
itemName,price,quantity가 sql에 있는 ? 에 순서대로 바인딩된다.
String sql = "update item set item_name=?,quantity=?,price=? where id = ?"
다른개발자가 만약 price와 quantity의 순서를 변경하면 가격에는 수량이, 수량에는 가격이 반영되는 대참사가 난다.
그래서 파라미터를 이름을 지정해서 바인딩하는 방법이 있다.
@Slf4j
@Repository
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";
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, 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) {
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() {
return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
}
}
이름을 기반으로 파라미터 바인딩을 하려면 JdbcTemplate=>NamedParameterJdbcTemplate 를 사용한다.
파라미터 전달시에는 Map처럼 Key,value구조를 만들어서 전달해야한다.
Map
findById()메서드에서 key로 "id", value로 id로 Map에 저장하여 해당 Map을 넘겨주었다.
MapSqlParameterSource
update()메서드에서 메서드체인을 사용해서 key value를 넣어줬다.
BeanPropertySqlParameterSource
자바빈 프로퍼티 규약을 통해서 파라미터객체를 자동적으로 생성한다.
ex). getXxx() -> xxx
save메서드에서 Item에 해당하는 필드명을 보고, 필드명을 key로, getXxx를 통해서 value를 가져와서 param에 넣어준다.
그러나, update()같이 SQL에 :id를 바인딩해야하는데 ItemUpdateDto에는 itemId가 없다. 따라서 BeanPropertySqlParameterSource 대신 MapSqlParameterSource을 사용했다.
BeanPropertyRowMapper(별칭)
이전에는 Item item = new Item();
item.setId..
item.setPrice...
이런식으로 만들었는데
BeanPropertyRowMapper를 사용하면 자바빈 규약에 맞춰서 데이터를 변환한다.
예외 1.
"select id, item_name, price, quantity from item where id = :id";
item에서는 itemName이 필드명이다. 하지만 대부분 DB에서는 item_name으로 대부분 표기를 한다. BeanPropertyRowMapper는 언더스코어 표기법을 카멜로 자동 변환해준다.
예외 2.
만약 객체이름과 컬럼이름이 완전히 다른경우에 price<->item_pirce라면
as를 사용해 price as item_price로 사용하면 된다.
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)
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를 사용한다.