스프링 부트 2.6.5 버전을 기준으로 작성됨
H2 데이터베이스 Version 2.2.224 (2023-09-17)
SQL을 직접 사용하는 경우 좋은 DB접근 기술이다.
spring-jdbc
라이브러리에 포함 (스프링 기본 사용 라이브러리)statement
를 준비 및 실행statement
, resultset
종료build.gradle
//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
private final JdbcTemplate template;
public JdbcTemplateItemRepository(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
JdbcTemplate
는 dataSource
가 필요
dataSource
를 의존 관계 주입을 받고 생성자 내부에서 JdbcTemplate
생성 (관례상 많이 사용)
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(connection -> {
//자동 증가 키
PreparedStatement ps = connection.prepareStatement(sql, new String[] {"id"});
//ps.setXxx로 파라미터 바인딩
return ps;
}, keyHolder);
long key = keyHolder.getKey().longValue();
PK 생성에 identity (auto increment) 방식을 사용했기에 id 값을 비워두고 저장
하지만 이러면 insert 가 완료되어야 생성된 PK id값을 확인 가능
-> KeyHolder
와 connection.prepareStatement(sql, new String[] {"id"})
를 사용해서 id를 지정해주면 insert 쿼리 실행 이후에 DB에서 생성된 id 값 조회 가능
// 여러 개가 있지만 여기서 사용된 메서드
PreparedStatement prepareStatement(String sql, String columnNames[]) throws SQLException;
@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();
}
}
queryForObject()
RowMapper
는 데이터베이스의 반환 결과인 ResultSet
을 객체로 변환EmptyResultDataAccessException
예외 발생IncorrectResultSizeDataAccessException
예외 발생ItemRepository.findById()
인터페이스는 결과가 없을 때 Optional
을 반환해야한다.
따라서 결과가 없으면 예외를 잡아서 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());
}
데이터를 리스트로 조회, 검색 조건에 따른 데이터 조회
template.query()
RowMapper
는 데이터베이스의 반환 결과인 ResultSet
을 객체로 변환JdbcTemplate가 루프를 돌려주고, 개발자는 RowMapper
를 구현해서 그 내부 코드만 채운다.
while(resultSet 이 끝날 때 까지) {
rowMapper(rs, rowNum);
}
findAll
에서 사용자가 검색하는 값에 따라 SQL이 동적으로 달라져야한다.
select id, item_name, price, quantity from item
select id, item_name, price, quantity from item where item_name like concat('%', ?, '%')
...
검색 조건의 개수는
1개의 검색조건이 있다 없다 하여 2가지 경우의 수이고
2개면 2x2
3개면 2x2x2
즉 검색 조건의 수가 n이라 했을 때
SQL문의 경우의 수는 2^n 이 된다.
또한 2가지 이상의 조건이면 and
를 넣어줘야하고 골치아프다.
MyBatis의 가장 큰 장점은 SQL을 직접 사용할 때 동적 쿼리를 쉽게 작성할 수 있다
@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);
}
}
@Slf4j
//@Import(MemoryConfig.class)
@Import(JdbcTemplateV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
// application.properties // src/main
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
스프링 부트가 해당 설정을 사용해서 커넥션 풀과 DataSource
, 트랜잭션 매니저를 스프링 빈으로 자동 등록한다.
// application.properties
#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
SQL 코드의 순서 변경시
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getQuantity(),
updateParam.getPrice(),
itemId);
가격이 수량이 되고 수량이 가격이되어버리는 큰 사고 발생
개발을 할 때는 코드를 몇줄 줄이는 편리함도 중요하지만, 모호함을 제거해서 코드를 명확하게 만드는 것이 유지보수 관점에서 매우 중요
이러한 문제들을 해결하기위해 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공
NamedParameterJdbcTemplate
를 사용
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
NamedParameterJdbcTemplate
도 DataSource
가 필요
@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;
}
SQL 에서 ?
-> :파라미터이름
으로 받는다.
insert into item(item_name, price, quantity) values (:itemName, :price, :quantity)
이름 지정 파라미터를 전달하려면 Map
처럼 Key
, value
데이터 구조를 만들어서 전달해야 한다.
key
: :파라미터이름
으로 지정한, 파라미터의 이름
value
: 해당 파라미터의 값
Map
SqlParameterSource
MapSqlParameterSource
BeanPropertySqlParameterSource
단순히 Map
사용
Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
Map
과 유사한데 SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능 제공
SqlParameterSource
인터페이스의 구현체
메서드 체인을 통해 편리한 사용법도 제공
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
// 파라미터 계속 추가
.addValue("id", itemId); // id를 넣는 부분이 별도로 필요
template.update(sql, param);
SqlParameterSource
인터페이스의 구현체
자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성
getXxx() -> xxx, getItemName() -> itemName
ex) getItemName()
key=itemNamem value=상품명 값
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
매우 간편하다.
여기서 주의할 점은
:id
바인딩을 해주어야하는 상황에서 파라미터에서 넘어온 Dto에 id값이 없다면 사용할 수 없다. 이때는MapSqlParameterSource
를 사용하자
기존 itemRowMapper 를 사용할 때는 람다식을 사용해서 하나하나 값을 세팅해주었다.
// 기존
private RowMapper<Item> itemRowMapper() {
return ((rs, rowNum) -> {
Item item = new Item();
item.setId(rs.getLong("id"));
// setter로 item 값 설정 ...
return item;
});
}
이번에는 BeanPropertyRowMapper를 사용해서 간편해졌다.
// new
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); // camel 변환 지원
}
BeanPropertyRowMapper
는 ResultSet
의 결과를 받아서 자바빈 규약에 맞추어 데이터를 반환
ex) DB 조회 결과가 select id, itemName
이라고 한다면 다음과 같은 코드를 작성해준다. (실제로는 리플렉션 같은 기능을 사용)
Item item = new Item();
item.setId(rs.getLong("id");
item.setItemName(rs.getString("itemName");
데이터베이스에서 조회한 결과 이름을 기반으로 setId()
, setPrice()
처럼 자바빈 프로퍼티 규약에 맞춘 메서드를 호출
컬럼 명에 _
이 중간에 포함되어 있거나
아예 다른 컬럼명이면 어떻게 해야할까?
이때는 as
를 사용하여 별칭을 붙여준다.
select item_name as itemName
select member_name as username
이러한 방법으로 자주 사용한다.
자바 객체는 카멜 표기법
관계형 데이터베이스는 스네이크 케이스 표기법(언더스코어 사용)
BeanPropertyRowMapper
는 언더스코어 표기법을 카멜로 자동 변환해준다.
select item_name
으로 조회 -> setItemName()
에 문제 없이 값이 들어간다.
참고
리플렉션이란?
구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
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"); 생략 가능 심플 jdbc가 item table의 메타 데이터를 읽음
}
SimpleJdbcInsert
도 dataSource를 받는다.
withTableName
: 데이터를 저장할 테이블 명 지정usingGeneratedKeyColumns
: key
를 생성하는 PK 컬럼 명을 지정usingColumns
: INSERT SQL에 사용할 컬럼을 지정, 특정 값만 저장하고 싶을 때 사용, 모든 컬럼 저장시 생략가능SimpleJdbcInsert
는 생성 시점에 데이터베이스 테이블의 메타 데이터를 조회
어떤 컬럼이 있는지 확인하기에 usingColumns
을 생략 가능
jdbcInsert.executeAndReturnKey(param)
을 사용해서 INSERT SQL을 실행하고, 생성된 키 값도 매우 편리하게 조회 가능
public Item save(Item item) {
SqlParameterSource param = new BeanParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
나머지 코드 부분은 기존과 같다.
@Override
public Item save(Item item) {
BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
JdbcTemplate
NamedParameterJdbcTemplate
SimpleJdbcInsert
SimpleJdbcCall
🔖 학습내용 출처