스프링 DB 2편

Seung jun Cha·2022년 11월 16일
0

1. JdbcTemplate

  • JDBC를 직접 사용할 때 발생하는 대부분의 문제를 해결해준다.
  1. 커넥션 획득
  2. statement 를 준비하고 실행
  3. 결과를 반복하도록 루프를 실행
  4. 커넥션 종료, statement , resultset 종료
  5. 트랜잭션 다루기 위한 커넥션 동기화
  6. 예외 발생시 스프링 예외 변환기 실행

그리고 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;
        });
    }

1-1 이름지정 파라미터

  • JdbcTemplate에서 파라미터를 바인딩할 때는 순서를 지키는 것이 중요하다.
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);
    }
  1. BeanPropertySqlParameterSource : Entity의 값을 가져와서 파라미터로 만들어주고 :에 맞는 것은 자동으로 세팅해주고 없는 것은 알아서 버린다.
 BeanPropertySqlParameterSource param = 
 new BeanPropertySqlParameterSource(item);
        
 template.update(sql, param, keyHolder);
  1. MapSqlParameterSource
  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);
    }
  1. Map.of
    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();
          }
      }
    1. BeanPropertyRowMapper : ResultSet의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환한다. BeanPropertyRowMapper는 언더스코어 표기법을 카멜로 자동 변환해준다.
private RowMapper<Item> itemRowMapper() {
 return BeanPropertyRowMapper.newInstance(Item.class); 
}

1-2 SimpleJdbcInsert

  • Insert SQL을 직접 작성하지 않아도 되는 편리한 기능을 제공한다

  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;
}
  1. withTableName : 데이터를 저장할 테이블 명을 지정한다.
  2. usingGeneratedKeyColumns : key를 생성하는 PK 컬럼 명을 지정한다.
    SimpleJdbcInsert는 생성시점에 테이블을 읽어서 테이블 이름만 알면 어떤 데이터를 가지고 있는지 알기 때문에 파라미터 이름을 따로 적을 필요가 없다.

2. 테스트

2-1 @BeforeEach, @AfterEach

  • 각 테스트를 실행할 때, 영향을 주지 않기 위해서는
    트랜잭션 시작 - 테스트 실행 - 테스트 종료 - 롤백 과정을 거쳐야 한다. 이 때 @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);
    }

2-2 @Trantactional, @Commit

  • 스프링에서의 @Transactional 은 로직이 성공적으로 수행되면 커밋하고 실패하면 롤백하도록 동작하지만, 테스트에서 @Transactional이 사용되면 테스트가 끝났을 때, 성공하든 실패하든 트랜잭션을 자동으로 롤백한다

  • @Commit : 롤백하지않고 강제 커밋을 하고 싶을 때, 사용하면 된다.

2-3 임베디드 모드

  • DB를 애플리케이션에 내장해서 함께 실행하는 모드로 애플리케이션이 종료되면 DB도 함께 종료되고 데이터도 모두 사라진다. 만약 properties에 데이터베이이스에 관한 정보가 설정되어 있지 않으면 스프링부트는 자동으로 임베디드 모드로 작동한다. 임베디드 모드는 메모리를 사용하기 때문에 DB가 꺼져 있더라도 문제없다.

스프링부트는 resources폴더아래 SQL스크립트를 생성, 사용해서 애플리케이션이 시작될 때, 데이터베이스를 초기화 하는 기능을 제공한다. 따라서 테스트를 진행할 때 테이블을 미리 만들어 놓을 수 있다.

3. MyBatis

3-1 설정

  1. mybatis.type-aliases-package : 마이바티스에서 타입 정보를 사용할 때는 전체 패키지 경로를 적어야 하는데, 여기에 명시하면 패키지
    이름을 생략할 수 있다.

  2. mybatis.configuration.map-underscore-to-camel-case=true : 언더바를 카멜로 자동 변경해주는 기능을 활성화한다.

  3. logging.level.hello.itemservice.repository.mybatis=trace

  4. @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);
}
  1. 매퍼 인터페이스와 이름, 경로가 동일하게 xml 파일을 생성한다. xml파일을 원하는 곳에 두고 싶다면 application.properties에 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="매퍼 인터페이스의 경로">

3-2 사용

  • Mapper.xml이 @Repository 역할을 한다.
  • id에는 매퍼 인터페이스의 메서드 명을 적으면 된다.
  • Controller에 파라미터가 1개만 있으면 @Param 을 지정하지 않아도 되지만, 파라미터가 2개 이상이면 @Param 으로 이름을 지정한다.
  1. insert : useGeneratedKeys 는 데이터베이스가 키를 생성해 주는 IDENTITY 전략일 때 사용한다. keyProperty는 생성되는 키의 이름을 지정한다.
<insert id="save" useGeneratedKeys="true" keyProperty="id">
  1. update
<update id="update">
 update item
 set item_name=#{updateParam.itemName}
 where id = #{id}
  1. select : select문은 resultTtpe나 resultMap 속성을 지정해야한다.resultType은 반환 타입을 명시하면 된다.
Optional<Item> findById(Long id);

<select id="findById" resultType="Item">
  1. 동적 쿼리 사용 : <where> <if test> if절을 만족하면 and를 where로 바꾸고 구문을 추가한다
    숫자의 크기를 비교할 때 <, >는 xml의 태그로 인식되어 사용할 수 없다.
< : &lt;
> : &gt;
& : &amp;
<where>
   <if test="itemName != null and itemName != ''">
   and item_name like concat('%',#{itemName},'%')
   </if>
 
   <if test="maxPrice != null">
   and price &lt;= #{maxPrice}
   </if>
 </where>

동적쿼리를 지원하는 것에는 다음과 같은 것들이 있다.


if
choose (when, otherwise)
trim (where, set)
foreach
resultMap..

@Mapper 인터페이스에 crud 설정 -> xml에 crud 구현 -> Repository에 Mapper를 주입해서 사용
여기서 Mapper인터페이스를 구현하지 않고도 사용할 수 있는 이유는 스프링에서 프록시 객체로 생성을 해주기 때문이다.

그래서 작동 순서는 mapper - service - controller - jsp

3-3 페이징 처리

  • limit의 뒤에는 식을 사용할 수 없고 값만 사용할 수 있다.
    시작페이지번호, 페이지당 게시물 개수, 건너뛰기 수,
    전체 데이터 수
limit 10  -> 한 페이지에 가져오는 데이터의 수
limit 10, 10 -> 건너뛰는 데이터의수, 가져오는 데이터의 수
5페이지라면 limit 40, 10 이 된다.
  • 페이지 처리를 할 때는 나중에 확장 여부를 고려해서라도 별도의 DTO로 만들어 두는 것이 좋다
@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>
  • vo목록과 전체 데이터 수를 서비스 계층에서 한 번에 담아서 처리하도록 ResponseDTO를 구성한다.
@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데이터

  1. page와 size가 담긴 requestDTO로 selectList를 출력한다. 화면에 페이지 번호를 구성하기 위해 전체 데이터의 수를 구할 수 있는 getCount 메서드를 만든다.

  2. mapper를 통해 List와 전체 데이터를 가지고 왔을 때, 이를 서비스 계층에서 한번에 처리할 수 있도록 DTO를 만든다.

  3. 페이지 번호가 붙을 때는 쿼리스트링으로 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} 
    이런식으로 사용

3-4 검색, 필터링처리

  1. 검색에 사용하는 문자열
  2. 기간 검색을 위한 날짜 변수 2개
  3. 검색 유형을 정할 String[]
  4. 완료여부 체크박스

0개의 댓글