스프링 부트 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 : 해당 파라미터의 값
MapSqlParameterSourceMapSqlParameterSourceBeanPropertySqlParameterSource단순히 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;
}
JdbcTemplateNamedParameterJdbcTemplateSimpleJdbcInsertSimpleJdbcCall🔖 학습내용 출처