그리고 JdbcTemplate를 사용하기 위해서는 DataSource가 필요하기 때문에 생성자로 주입해준다.
private final JdbcTemplate template;
public JdbcTemplateItemRepository(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
queryForObject()
: 반환 값이 1개일 때 사용query()
: 반환 값이 여러개일때 사용객체 값을 찾아서 가지고 올 때는 RowMapper
이 필요하다. RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환한다.
1편 게시글에 적었지만 다시 적는다. ResultSet은 SQL문에 대한 결과를 저장하는 곳으로 내부에 있는 자료를 가르키는 커서를 통해 next() 함수로 데이터를 가지고 온다.
try {
connection = getConnection();
connection.prepareStatement(sql);
preparedStatement.setString(1, memberId);
rs = preparedStatement.executeQuery();
if (rs.next()){ // 선택한 멤버의 값을 새 멤버에 세팅
Member member = new Member();
member.setMemberId(rs.getString("memberId"));
member.setMoney(rs.getInt("money"));
return member;
}else {
throw new NoSuchElementException
("member not found memberId=" + memberId);
}
RowMapper
는 next()를 사용해 커서로 다음 자료를 가져오는 과정을 마지막 값까지 JdbcTemplate이 대신 해준다는 것이다. 뒤에 나오는 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;
});
}
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql, itemName, price,quantity, itemId);
문제를 파라미터를 추가하거나 수정하는 경우인데 실수로 데이터베이스에 데이터가 잘못들어가서 복구를 해야하는 상황이 오면 굉장히 힘들어진다.
이를 위해 순서가 아닌 이름이 같은 경우에 바인딩을 하는 방법으로
?
가 아니라 :
를 사용하여 바인딩을 하며 아래 세 가지 모두 같은 기능을 제공하므로 원하는 것을 골라서 사용하면 된다.
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
:
에 맞는 것은 자동으로 세팅해주고 없는 것은 알아서 버린다. BeanPropertySqlParameterSource param =
new BeanPropertySqlParameterSource(item);
template.update(sql, param, keyHolder);
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name=:itemName, price=:price,
quantity=quantity where id=:id";
MapSqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
template.update(sql, param);
}
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id=:id";
try {
Map<String, Long> param = Map.of("id", id);
Item item = template.queryForObject(sql,param, itemRowMapper());
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class);
}
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");
}
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
withTableName
: 데이터를 저장할 테이블 명을 지정한다.usingGeneratedKeyColumns
: key를 생성하는 PK 컬럼 명을 지정한다.트랜잭션 시작 - 테스트 실행 - 테스트 종료 - 롤백
과정을 거쳐야 한다. 이 때 @BeforeEach
, @AfterEach
를 사용한다. 테스트를 트랜잭션 단위로 시작하지않고 롤백하지 않으면 테스트 과정에서 만들어진 데이터가 계속 쌓이기 때문에 정상적인 테스트가 불가능하다. @Autowired
PlatformTransactionManager transactionManager;
TransactionStatus transaction;
@BeforeEach // 테스트 시작 전 트랜잭션을 가지고옴
public void beforeEach(){
transaction = transactionManager
.getTransaction(new DefaultTransactionDefinition());
}
@AfterEach // 테스트가 끝난 후 롤백
public void afterEach(){
if (itemRepository instanceof MemoryItemRepository){
((MemoryItemRepository) itemRepository).clearStore();
}
transactionManager.rollback(transaction);
}
스프링에서의 @Transactional 은 로직이 성공적으로 수행되면 커밋하고 실패하면 롤백하도록 동작하지만, 테스트에서 @Transactional이 사용되면 테스트가 끝났을 때, 성공하든 실패하든 트랜잭션을 자동으로 롤백한다
@Commit
: 롤백하지않고 강제 커밋을 하고 싶을 때, 사용하면 된다.
스프링부트는 resources폴더아래 SQL스크립트를 생성, 사용해서 애플리케이션이 시작될 때, 데이터베이스를 초기화 하는 기능을 제공한다. 따라서 테스트를 진행할 때 테이블을 미리 만들어 놓을 수 있다.
mybatis.type-aliases-package
: 마이바티스에서 타입 정보를 사용할 때는 전체 패키지 경로를 적어야 하는데, 여기에 명시하면 패키지
이름을 생략할 수 있다.
mybatis.configuration.map-underscore-to-camel-case=true
: 언더바를 카멜로 자동 변경해주는 기능을 활성화한다.
logging.level.hello.itemservice.repository.mybatis=trace
@Mapper
를 붙인 마이바티스 매핑 XML을 호출해주는 매퍼 인터페이스를 생성한다. 파라미터가 두 개 이상일때 @Param을 사용한다.
@Mapper
public interface ItemMapper {
void save(Item item);
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto
updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond itemSearch);
}
mybatis.mapper-locations=classpath:mapper/**/*.xml
를 설정하면 된다. 그러면 resources/mapper 하위의 xml파일을 자동으로 인식한다. 인터페이스의 메서드를 호출하면 xml에서 해당 SQL을 실행하고 결과를 돌려준다. <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="매퍼 인터페이스의 경로">
메서드 명
을 적으면 된다.useGeneratedKeys
는 데이터베이스가 키를 생성해 주는 IDENTITY 전략일 때 사용한다. keyProperty
는 생성되는 키의 이름을 지정한다. <insert id="save" useGeneratedKeys="true" keyProperty="id">
<update id="update">
update item
set item_name=#{updateParam.itemName}
where id = #{id}
resultType
은 반환 타입을 명시하면 된다.Optional<Item> findById(Long id);
<select id="findById" resultType="Item">
<where> <if test>
if절을 만족하면 and를 where로 바꾸고 구문을 추가한다<
, >
는 xml의 태그로 인식되어 사용할 수 없다.< : <
> : >
& : &
<where>
<if test="itemName != null and itemName != ''">
and item_name like concat('%',#{itemName},'%')
</if>
<if test="maxPrice != null">
and price <= #{maxPrice}
</if>
</where>
동적쿼리를 지원하는 것에는 다음과 같은 것들이 있다.
if
choose (when, otherwise)
trim (where, set)
foreach
resultMap..
@Mapper 인터페이스에 crud 설정 -> xml에 crud 구현 -> Repository에 Mapper를 주입해서 사용
여기서 Mapper인터페이스를 구현하지 않고도 사용할 수 있는 이유는 스프링에서 프록시 객체로 생성을 해주기 때문이다.
그래서 작동 순서는 mapper - service - controller - jsp
limit 10 -> 한 페이지에 가져오는 데이터의 수
limit 10, 10 -> 건너뛰는 데이터의수, 가져오는 데이터의 수
5페이지라면 limit 40, 10 이 된다.
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default
@Min(value = 1)
@Positive
private int page = 1; 페이지 번호
@Builder.Default
@Min(value = 1)
@Max(value = 100)
@Positive
private int size = 10; 페이지당 글 개수
public int getSkip(){
return (page - 1) * 10; 페이지 번호가 넘어갈 때 건너뛰는 글의 개수
}
}
마이바티스는 get,set으로 동작하기 떄문에 여기서 #{skip}
는 메서드 getSkip()을 호출한다
<select id="selectList" resultType="org.zerock.springex.domain.TodoVO">
select * from tbl_todo order by tno desc limit #{page}, #{size}
</select>
@Getter
@ToString
public class PageResponseDTO2<E> {
private int page;
private int size;
private int total;
private int start; // 시작페이지 번호
private int end; // 끝 페이지 번호
private boolean prev; // 이전페이지 존재여부
private boolean next; // 다음페이지 존재여부
private List<E> dtoList;
// mapper의 selectList를 사용해 가지고 온 voList를 dto로 변환해서 담을 List
@Builder(builderMethodName = "withAll")
public PageResponseDTO2(PageRequestDTO2 pageRequestDTO2,
List<E> dtoList, int total) {
this.page = pageRequestDTO2.getPage();
this.size = pageRequestDTO2.getSize();
this.total = total;
this.dtoList = dtoList;
this.end = (int)(Math.ceil(this.page / 10.0 )) * 10;
this.start = this.end - 9;
int last = (int)(Math.ceil((total/(double)size)));
// 전체 개수를 고려한 마지막 페이지
this.end = end > last ? last: end;
this.prev = this.start > 1;
this.next = total > this.end * this.size;
}
requestDTO : 시작페이지번호, 페이지당 게시물 개수, 건너뛰기 수
responseDTO<> : 시작페이지번호, 페이지당 게시물 개수, 전체 데이터 수, 끝페이지 번호, 이전페이지 존재여부, 다음페이지 존재여부, 리스트로 보여줄 DTO데이터
page와 size가 담긴 requestDTO로 selectList를 출력한다. 화면에 페이지 번호를 구성하기 위해 전체 데이터의 수를 구할 수 있는 getCount 메서드를 만든다.
mapper를 통해 List와 전체 데이터를 가지고 왔을 때, 이를 서비스 계층에서 한번에 처리할 수 있도록 DTO를 만든다.
페이지 번호가 붙을 때는 쿼리스트링으로 page와 size를 같이 전달해 주어야 조회, 수정, 삭제 페이지에서 다시 목록으로 이동할 때 기존 페이지를 볼 수 있다. 페이지 이동정보가 있는 클래스에 메서드를 작성한다
private String link;
public String getLink(){
if (link == null){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("page=" + this.page);
stringBuilder.append("&size=" + this.size);
link = stringBuilder.toString();
}
return link;
}
"/todo/read?tno=${dto.tno}&${pageRequestDTO.link}
이런식으로 사용