1. JdbcTemplate
1) JdbcTemplate 장단점
장점
- 반복 문제 해결
- 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할 때 발생하는 반복 작업 대신 처리
- 개발자는 SQL 작성, 전달할 파라미터 정의, 응답 값 매핑
- 커넥션 획득
- statement 준비, 실행
- 결과 반복
- 커넥션 종료, statement, resultset 종료
- 커넥션 동기화
- 예외 발생시 스프링 예외 변환기 실행
- 설정 편리
- 스프링으로 JDBC를 사용할 때 기본으로 사용되는 spring-jdbc 라이브러리에 포함되어 있음
단점
- 동적 SQL 해결하기 어려움
2) JdbcTemplate 설정
* build.gradle
dependencies {
//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'
}
- JdbcTemplate 추가 -> JdbcTemplate이 들어있는 spring-jdbc가 라이브러리에 포함됨
- H2 데이터베이스에 접속 -> H2 데이터베이스의 클라이언트 라이브러리 (Jdbc Driver) 추가
2. JdbcTemplate 적용
2-1. 기본
0) ItemRepository 인터페이스
package hello.itemservice.repository;
import hello.itemservice.domain.Item;
import java.util.List;
import java.util.Optional;
public interface ItemRepository {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond cond);
}
1) ItemRepository 인터페이스 구현
package hello.itemservice.repository.jdbctemplate;
/**
* JdbcTemplate
*/
@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
- JdbcTemplateItemRepositoryV1은 ItemRepository 인터페이스를 구현함
- JdbcTemplate을 사용하기 위해 DataSource가 필요함 (의존 관계 주입, 생성자 내부에서 JdbcTemplate 생성)
-> 관례상 이렇게 많이 씀
( * JdbcTemplate을 스프링 빈으로 직접 등록하고 @Autowired 등으로 주입받아도 됨)
2) save() 상품 저장
@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;
}
- template.update()
- update : 데이터 변경 시 (Insert, Update, Delete)
- template.update()의 반환값은 int - 영향 받은 로우 수 반환
- KeyHolder (그냥 이런 방식이 있구나 참고)
- connection.prepareStatement(sql, new String[]{"id"})
- 데이터를 저장할 때 PK 생성에 identity (auto increment) 방식을 사용중
-> PK인 ID 값을 개발자가 직접 지정하는 것이 아니라 비워두고 저장
-> 데이터베이스가 ID를 대신 생성
- 데이터를 저장할 때 PK 생성에 identity (auto increment) 방식을 사용중
- long key = keyHolder.getKey().longValue()
- 데이터베이스에 INSERT가 완료되어야 생성된 PK ID값을 확인할 수 있음
- PK인 "id" 지정 -> INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID값 조회할 수 있음
- connection.prepareStatement(sql, new String[]{"id"})
3) update() 상품 수정
@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);
}
- template.update()
- ?에 바인딩할 파라미터를 순서대로 전달
- 반환값은 쿼리의 영향을 받은 로우 수 (int)
-> 여기서는 where id=? 를 지정했기 때문에 1개만 영향받음
4) findById() 상품 1개 조회
@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();
}
}
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;
});
}
(1) template.queryForObject(SQL, 반환타입(RowMapper<T>), 인자)
- 결과값이 1개일 때
- 반환타입 : 데이터 형만 가능 (Long, Integer, String 등)
-> 그래서 RowMapper를 사용해서 원하는 형태의 결과값 반환 (반환 결과인 ResultSet을 객체로 변환) - 결과가 없으면 EmptyResultDataAccessException 예외
- 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외
(2) RowMapper<T>
- ResultSet에 담긴 쿼리 결과값을 원하는 형태로 변환
- ResultSet은 직접 next()를 확인해서 루프를 돌렸지만, RowMapper는 내부 코드만 채우면 루프를 알아서 돌려줌
- rowNum : 현재 행의 수 ( null 가능, 행의 수만큼 ResultSet 값과 객체를 매핑시킴 )
- -> 쿼리 결과값이 ResultSet에 담김
-> 원하는 객체를 생성해서 거기에 ResultSet의 값을 매핑시킴
-> 객체 반환
-> 이걸 rowNum 만큼 실행
(3) Optional<T>
- ItemRepository.findById() 인터페이스는 결과가 없을 때 Optional 반환해야 함
-> 결과가 없으면 예외를 잡아서 Optional.empty() 로 반환 - null이 올 수 있는 값을 감싸는 Wrapper 클래스 -> NPE(Null Point Exception)가 발생하지 않도록 해줌
- Optional.empty()
- 값이 null 일 수 있을 때
-> Optional 빈 객체 생성
-> Optional로 감싸진 객체를 반환하기 때문에 객체를 get하기 전에 null 체크해야 하는 것을 인식할 수 있음
- 값이 null 일 수 있을 때
- Optional.of()
- 값이 절대 null이 아닐 때
-> 객체를 Optional로 Wrapping 해줌
- 값이 절대 null이 아닐 때
- Optional.ofNullable()
- 값이 null일 수도, 아닐 수도 있을 때
-> orElse, orElseGet 등으로 값이 없는 경우에도 안전하게 가져올 수 있음
-> null 이면 Optional.empty() 를 반환
- 값이 null일 수도, 아닐 수도 있을 때
- Optional.empty()
💡 Optional 사용 예
if ( ~~ != null)
if ( ~~ != null)
if (~~ != null)
같이 복잡하지 않게 Optional 사용
Optional.ofNullalble(~~) . orElse( ~~~) . ~~
5) findAll() 검색 결과 목록 조회
@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;
});
}
- template.query(SQL, 반환타입(RowMapper<T>), 인자)
- 결과가 1개 이상일 때
- 결과가 없으면 빈 컬렉션 반환
- 동적 쿼리 -> JDBC Template에서 동적 쿼리를 작성하기 매우 복잡함
(어떤 경우 where넣고, 어떤 경우 and넣고 이런 경우를 모두 계산해야 함)
2-2. DB 연결 설정
1) JdbcTemplateV1Config
- JdbcTemplateItemRepositoryV1을 스프링 빈에 등록
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV1;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@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);
}
}
- 메모리 저장소가 아니라 실제 DB에 연결하는 JdbcTemplate이 사용되도록 설정 Configuration
- itemService 인터페이스, itemRepository 구현체로 JdbcTemplateRepository가 사용되도록 설정
- DataSource는 스프링 부트가 자동 등록하도록 application.properties에서 설정 -> dataSource를 주입받음
2) ItemServiceApplication 변경
package hello.itemservice;
import hello.itemservice.config.*;
import hello.itemservice.repository.ItemRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
@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);
}
}
- 원래 @Import(MemoryConfig.class) -> @Import(JdbcTemplateV1Config.class) 변경
3) application.properties 데이터베이스 접근 설정
spring.profiles.active=local
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
- 스프링부트가 해당 설정을 사용해서 커넥션 풀, DataSource, 트랜잭션 매니저를 스프링 빈으로 자동 등록 !!
4) 로그 추가
#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug
- SQL 로그 확인 -> main, test 둘다 확인하려면 두 곳에 모두 추가
3. NamedParameterJdbcTemplate - 이름 지정 파라미터
3-1. 이름 지정 파라미터 바인딩
* 원래 순서대로 파라미터 연결
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
순서 꼬이거나 데이터 추가되는 경우 -> 버그
-> 이름 지정 파라미터 바인딩 사용
1) NamedParameterJdbcTemplate 주입
/**
* NamedParameterJdbcTemplate
*/
@Slf4j
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
- 생성자 내부에서 DataSource 주입받음 (JdbcTemplate => DataSource 필요), NamedParameterJdbcTemplate 생성
- NamedParameterJdbcTemplate를 스프링 빈으로 직접 등록하고 주입받아도 됨
2) SQL 수정
- ? -> :파라미터이름
// save
String sql = "insert into item(item_name, price, quantity) values (:itemName, :price, :quantity)";
// update
String sql = "update item " +
"set item_name=:itemName, price=:price, quantity=:quantity " +
"where id=:id";
// findById
String sql = "select id, item_name, price, quantity from item where id=:id";
3) 파라미터 바인딩
* 파라미터 종류 3가지
- Map
- SqlParameterSource 인터페이스
- MapSqlParameterSource
- BeanPropertySqlParameterSource
(1) Map 사용
* findById()
@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();
}
}
- new HashMap() 으로 해도 되지만, 간단히 넣기 위해 Map.of() 사용
- 파라미터 전달하려면 key, value 구조를 만들어서 전달
- key = :파라미터이름 으로 지정한 파라미터의 이름 , value = 해당 파라미터의 값
- queryForObject(sql, param, itemRowMapper());
(2) MapSqlParameterSource 사용
* update()
@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);
}
- SqlParameterSource 인터페이스의 구현체
- Map과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 더 특화된 기능
- 메소드 체인을 통한 편리한 사용법
(3) BeanPropertySqlParameterSource 사용
* save(), findAll()
@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 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());
}
- SqlParameterSource 인터페이스의 구현체
- 자바빈 프로퍼티 규약을 통해 자동으로 파라미터 객체 생성
- 예 ) new BeanPropertySqlParameterSource(item)
- Item 객체의 getItemName(), getPrice() 를 사용해 파라미터 자동으로 생성
- key = itemName , value = item 객체의 상품명 값
- key = price, value = item 객체의 가격 값
- 가장 좋아보이지만, 항상 사용할 수 있는 것 X
- update() 에서는 SQL에 :id를 바인딩 해야 하는데, ItemUpdateDto에는 itemId가 없음
-> BeanPropertySqlParameterSource 사용 X
-> MapSqlParameterSource 사용해서 따로 추가
- update() 에서는 SQL에 :id를 바인딩 해야 하는데, ItemUpdateDto에는 itemId가 없음
(4) BeanPropertyRowMapper 사용
* 원래
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;
});
}
* BeanPropertyRowMapper 사용
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); // camel 변환 지원
}
- ResultSet의 결과를 받아서 자바빈 규약에 맞춰 데이터 변환
- 쿼리 조회 결과 "id" 값 -> setId() 호출 -> Item 객체의 Id 값에 적용
- 쿼리 조회 결과 "item_name" 값 -> setItemName() 호출 -> 카멜 표기법으로 자동 변환 -> Item 객체의 itemName 값에 적용
- 관례의 불일치
- 자바 객체 : 카멜 표기법(camelCase)
- 관계형 데이터베이스 : 언더스코어 표기법(snake_case)
- -> BeanPropertyRowMapper는 언더스코어 표기법을 카멜로 자동 변환해줌
-> select item_name 값이 setItemName() 에 잘 들어감
3-2. DB 연결 설정
1) JdbcTemplateV2Config
- JdbcTemplateItemRepositoryV2 를 스프링 빈에 등록
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV1;
import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV2;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
@RequiredArgsConstructor
public class JdbcTemplateV2Config {
private final DataSource dataSource;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JdbcTemplateItemRepositoryV2(dataSource);
}
}
2) JdbcTemplateV2Config를 사용하도록 변경
@Import(JdbcTemplateV2Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
...
}
- @Import(JdbcTemplateV1Config.class) -> @Import(JdbcTemplateV2Config.class)
4. SimpleJdbcInsert - INSERT SQL 작성해주는 기능
* Repository - SimpleJdbcInsert 주입
/**
* 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"); // 생략 가능
}
- DataSource를 의존성 주입 받음
- 생성자 내부에서 SimpleJdbcInsert를 생성함
- withTableName : 테이블 지정
- usingGenerateKeyColumns : key를 생성하는 PK 컬럼명 지정
- usingColumns : INSERT SQL에 사용할 컬럼 지정 (생략 가능 -> 특정 값만 저장하고 싶을 때 사용)
-> 생성 시점에 테이블의 데이터를 조회해서 알아서 컬럼 확인함
* save()
@Override
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
- jdbcInsert.executeAndReturnKey(param) : INSERT SQL 생성, 생성된 키 값 편리하게 조회
* Config 객체에 JdbcTemplateItemRepositoryV3 스프링 빈 등록
* Application 객체에 @Import(JdbcTemplateV3Config.class) 를 설정으로 사용하도록 변경
5. JdbcTemplate 기능 정리
1) JdbcTemplate이 제공하는 주요 기능
- JdbcTemplate
- 순서 기반 파라미터 바인딩 지원
- 순서 기반 파라미터 바인딩 지원
- NamedParameterJdbcTemplate
- 이름 기반 파라미터 바인딩 지원
- SimpleJdbcInsert
- INSERT SQL 편리하게 사용
2) JdbcTemplate 사용법
(1) 조회
* 1건 조회 - 숫자
- 1건 조회 - queryForObject()
- 조회 대상이 객체가 아니라 단순 데이터 하나 - Integer.class, String.class 처럼 지정
int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
* 1건 조회 - 숫자, 파라미터 바인딩
int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
* 1건 조회 - 문자
String lastName = jdbcTemplate.queryForObject("select last_name from t_actor where id = ?", String.class, 1212L);
* 1건 조회 - 객체
- RowMapper - 결과를 객체로 매핑
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);
* 목록 조회 - 객체
- 여러 행 - query()
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;
});
- RowMapper 분리
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
};
public List<Actor> findAllActors() {
return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}
(2) 변경
* 등록
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));
(3) 기타
임의의 SQL 실행할 때는 execute() 사용
* DDL
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
=> 간단한 방법으로 SQL 사용
하지만, 동적 쿼리 해결 X + SQL을 자바 코드로 작성(코드가 라인 넘어갈 때마다 + 로 연결)
'Spring' 카테고리의 다른 글
[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (2) MyBatis (0) | 2023.07.06 |
---|---|
[인프런/스프링 DB 2편] 3. 데이터 접근 기술 - 테스트와 DB 연동 (0) | 2023.06.30 |
[인프런/스프링 DB 2편] 1. 데이터 접근 기술 (0) | 2023.06.28 |
[인프런/스프링 DB 1편] 6. 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2023.06.26 |
[인프런/스프링 DB 1편] 5. 자바 예외 (0) | 2023.06.24 |