만약 SQL을 직접 작성한다면 JdbcTemplate는 좋은 선택이다. jdbc를 깔면 spring-jdbc라이브러리를 사용할때 기본적으로 jdbc를 사용하는데 안에 같이 들어있고 세션문제, 트랙잭션 문제, 반복코드 작성 등을 알아서 해결해주기 때문.
gradle 설정 파일에 해당 라이브러리를 설치하고 사용한다고 설정하면 된다.
테이블은 직접 만들어야 한다.
id에다가 generated by default as identity를 사용하는데 그냥 저장만 하면 db보고 알아서 고유값을 만들어달라고 하는거다. 여기선 단순 숫자 증가만 할거임.
/**
* JdbcTemplate
*/
@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());
}
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;
});
}
}
생성자로 dataSource를 받는데 jdbctemplate를 사용할 때는 관례상 생성자로써 받는 편이다. 원한다면 스프링 빈에 등록해놓고 받아도 됨.
save()는 keyholder라고 이상한게 나오는데 이게 나오는 이유는 아까 db에서 id는 알아서 만들어주도록 설정했기 때문에 id를 알려면 일단 저장을 해야 한다. 문제는 여기 서버에선 db보고 알아서 pk id를 만들라고 했기 때문에 id를 제외한 모든 정보를 넣어서 저장함. 그래서 저장한 뒤에 id를 받아오려고 keyholder라는걸 사용하는 거다. 물론 keyholder없이 쌩 jdbc로 가져올 수도 있지만 너무 복잡하다. 나중엔 jdbctemplate가 제공하는 simplejdbcinsert라는게 있어서 이걸 사용하면 더 편리하게 가져올 수 있다.
findById에서는 RowMapper를 잘 사용하면 된다. 저번을 잘 생각해보면 rs는 db버전 포인터라서 rs.next를 사용하며 돌았던 기억이 있을거다. 그 while문을 대신 돌려주는것. 그래서 없으면 EmptyResultDataAccessException예외가, 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 나온다. 문제는 없으면 예외가 발생하기 때문에 catch문으로 잡아서 Optional empty를 반환해야 함.
finaAll은 동적쿼리라서 조금 복잡한데 동적쿼리가 뭐냐면 클라이언트가 준 상황에 따라 where를 할지, like를 쓸지 말지 등 sql문이 상황에 따라 변하는거임. 여기선 검색, 필터 기능이 있었기 때문에 생긴거다.
이걸 이런식으로 일일히 짜다보면 고려할게 너무 많이 분명 버그가 생길거다.
이렇게 설정해놓고 실행하면 잘 됨을 알 수 있다.
어떤 sql문을 실행하는지 상세히 보고 싶으면 설정파일에서 디버그 추가하면 된다.
이 버전1의 문제는 ? ? ? 로 맞추느라 순서가 바뀌면 엉뚱한게 db에 저장되는 대참사가 일어난다. 또 누가 아무생각없이 추가할 수도 있다. 진짜 큰일난다...
그래서 순서대신 이름을 사용하는 NamedParameterJdbcTemplate를 사용한다.
/**
* NamedParameterJdbcTemplate
* SqlParameterSource
* - BeanPropertySqlParameterSource
* - MapSqlParameterSource
* Map
*
* BeanPropertyRowMapper
*
*/
@Slf4j
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)";
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) {
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 ((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;
// });
return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
}
}
일부러 이름 지정 바인딩을 여러 방법을 썼는데, Map, MapSqlParamterSource, BeanPropertySqlParamterSource 3가지 방법을 사용했다.
Map의 경우는 별거없이 걍 Map을 사용한거고, MapSqlParameterSource는 Map과 유사하지만 sql 타입을 지정할 수 있는 등 좀 더 특화된 기능을 제공한다. BeanPropertySqlParameterSource는 인터페이스인 SqlParamterSource를 구체화하여 스프링이 해당 클래스를 보고 적절하게 param을 생성한다고 보면 된다. 객체의 자바 빈 규약으로 이름 및 함수가 규칙을 통해 만들어진다는 것을 활용해 Map에서 만든것을 자동으로 만들어주는 거다. 예를들어 해당 객체의 속성에 name, price가 있으면 이걸 lombok을 통하든 뭘 하든 name과 price를 얻는 함수 이름은 getName(), getPrice()일 것이고 이걸 사용해 Map에 자동으로 Map.of({"name", getName()}, {"price", getPrice()}) 같은 코드를 자동으로 작성해서 주는거다.
RowMapper를 보면 db의 컬럼에다가는 snake로 저장했는데 camel로 자동으로 바꿔서 지원해주는것도 확인할 수 있다. 이런 문제가 생긴 이유는 자바는 camel case를 사용하고 db는 대소문자를 구별하지 않기 때문에 snake case를 사용해서 불러올 때 이름이 다른 문제가 있었다. 그래서 만약 직접 sql문을 사용해서 불러올 땐 item_name as itemName으로 사용해도 되고, BeanPropertyRowMapper가 자동으로 해준다.
적용하려면 설정파일들만 잘 해주면 된다. 이제 인터페이스로 사용하기 때문에 숫자만 살짝 바꾸면 된다.
그럼 잘 된다.
부록으로 SimpleJdbcInsert라는게 있는데, insert를 더 편리하게 해주는거다. insert에서만 지원함.
JdbcTemplate의 주요 기능을 정리하면 주요 기능은 JdbcTemplate, NamedParamterJdbcTemplate, SimpleJdbcInsert, SimpleJdbcCall이 있는데 SimpleJdbcCall은 스토어드 프로시저를 편리하게 호출할 수 있다.
사용법은 하나의 row를 조회하는 단건 조회는 queryForObject를 사용하고 반환하는건 sql문의 결과에 따라 단순 숫자, string, 객체를 반환할 수도 있다.
목록을 조회할 땐 query 함수를 사용하면 된다. 목록이라 주로 복수개의 객체를 반환할 때 사용할거다.
insert, update, delete 함수도 있는데 숫자를 반환한다. 이것으로 인해 변화되는 row 수를 뜻한다.
기타로는 execute라는게 있는데 jdbcTemplate가 사전에 정의한 함수 외에 db의 기능을 사용하고 싶을 때.
이런 JdbcTemplate는 실무에서 간단하고 실용적인걸 찾을 때 사용하면 된다. JPA같은 ORM 기술을 사용하면서 동시에 sql을 직접 작성하는 일이 필요할 때도 사용하면 된다. 단점은 동적 쿼리문을 해결하지 못한다는 것. 그래서 다음에 동적 쿼리 문제를 해결하면서 동시에 SQL도 편리하게 작성할 수 있게 도와주는 MyBatis 기술을 사용해본다.
'CS > 김영한 스프링 강의' 카테고리의 다른 글
스프링 DB 2편 - 데이터 접근 핵심 원리 - 섹션4. 데이터 접근 기술 - MyBatis (0) | 2023.08.20 |
---|---|
스프링 DB 2편 - 데이터 접근 핵심 원리 - 섹션3. 데이터 접근 기술 - 테스트 (0) | 2023.08.20 |
스프링 DB 2편 - 데이터 접근 핵심 원리 - 섹션1. 데이터 접근 기술 - 시작 (0) | 2023.08.19 |
스프링 DB 1편 - 데이터 접근 핵심 원리 - 섹션6. 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2023.08.17 |
스프링 DB 1편 - 데이터 접근 핵심 원리 - 섹션5. 자바 예외 이해 (0) | 2023.08.15 |