일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- Oracle
- TodayILearned
- 스프링
- 생활코딩
- SpringMVC
- progressive web app
- 국비지원
- TIL
- 자바프로그래밍
- CSS
- sqldeveloper
- springaop
- framework
- mybatis
- javaprogramming
- tdd
- web
- javascript
- 프레임워크
- sql
- 서브쿼리
- PWA
- HTML
- js
- 자바스크립트
- JavaScript 내장객체
- maven
- 오라클
- 프로그레시브웹앱
- 메이븐
- Today
- Total
1cm
자바 프로그래밍_Day_98_Framework : MyBatis 동적 쿼리 / 게시글 조회 본문
자바 프로그래밍_Day_98_Framework : MyBatis 동적 쿼리 / 게시글 조회
dev_1cm 2022. 1. 12. 04:502022. 01. 05
동적쿼리 / 게시글 조회 실습
> BoardServiceTest.java
@ParameterizedTest
@MethodSource
위 테스트에 파라미터를 전해줄 메소드를 만드는 작업
그리고 밑 부분에 메소드를 만들어 줌
public static Stream<Arguments> filterProvider() {
// Stream 만들어서 리턴, 안에는 파라미터로 쓸 Arguments 리스트를 만들어서 넘겨줌
return Stream.of(
Arguments.arguments((Object) new String[]{"B2", "B3"}),
Arguments.arguments((Object) new String[]{"B1"})
);
}
여기서 반환해주는 값을 가지고 파라미터로 쓸 것임
@ParameterizedTest
@MethodSource("filterProvider")
public void getBoardCountTest(String[] filters) {
int count = 0;
// 현재 게시글의 갯수 가져오기
count = service.getBoardCount(filters);
System.out.println(count);
// 게시글의 숫자는 양수이면서, 0보다 크다는 것이라고 가정
assertThat(count).isPositive().isGreaterThan(0);
}
그 다음 윗 부분 getBoardCountTest 수정
그리고 테스트 진행
파라미터가 여러 개 있으면 arguments 밑에 여러 개 만들어주면 된다.
@ParameterizedTest
@MethodSource("filterProvider")
public void findAllTest(String[] filters) {
// 배열로 데이터 가져오기
// filters에는 B2, B3 값을 가지고 있다고 가정할 수 있음
// String[] filters = new String[] {"B2", "B3"};
// -> request.getParameterValues("filter");
List<Board> list = null;
// 리스트 얻어오기 (from. service 객체)
list = service.findAll(filters);
// System.out.println(list);
assertThat(list).isNotNull();
}
findAllTest도 수정해줬다.
- 페이징 적용
기존에는 rownum과 서브쿼리로 만들었는데, 조회되는 데이터가 바뀌면 쿼리문 여러 곳 수정해야한다는 불편함이 있었음
MyBatis에서는 RowBounds라는 클래스를 제공해줘서 그 클래스를 활용해 페이징 처리가 가능하다.
@ParameterizedTest
@MethodSource("filterProvider")
public void findAllTest(String[] filters) {
// 배열로 데이터 가져오기
// filters에는 B2, B3 값을 가지고 있다고 가정할 수 있음
// String[] filters = new String[] {"B2", "B3"};
// -> request.getParameterValues("filter");
List<Board> list = null;
PageInfo pageInfo = null;
int listCount = 0;
listCount = service.getBoardCount(filters);
// 페이지 info가져오기(현재 페이지, 페이징 몇개 보여줄지, 전체 게시글의 수, 한 페이지에서 보여줄 수)
pageInfo = new PageInfo(1, 10, listCount, 10);
// 리스트 얻어오기 (from. service 객체)
list = service.findAll(filters, pageInfo);
// System.out.println(list);
System.out.println(list.size()); assertThat(list).isNotNull();
}
기존에 있던 파라미터에 파라미터 값만 추가시켜주면 됨
따로 쿼리문을 건들이지 않고 작업한다.
> BoardService.java
public List<Board> findAll(String[] filters,PageInfo pageInfo) {
List<Board> list = null;
SqlSession session = getSession();
// dao에게 findAll 요청 하면서 매개값 session, filter 넘겨줌 (추가로 pageInfo도)
list = dao.findAll(session, filters, pageInfo);
return list;
}
pageInfo를 파라미터 값에 추가시켜주고
에러 해결을 위해
Dao의 파라미터 값도 추가시켜준다.
@ParameterizedTest
@MethodSource("filterProvider")
public void findAllTest(String[] filters) {
// 배열로 데이터 가져오기
// filters에는 B2, B3 값을 가지고 있다고 가정할 수 있음
// String[] filters = new String[] {"B2", "B3"};
// -> request.getParameterValues("filter");
List<Board> list = null;
PageInfo pageInfo = null;
int listCount = 0;
listCount = service.getBoardCount(filters);
// 페이지 info가져오기(현재 페이지, 페이징 몇개 보여줄지, 전체 게시글의 수, 한 페이지에서 보여줄 수)
pageInfo = new PageInfo(1, 10, listCount, 10);
// 리스트 얻어오기 (from. service 객체)
list = service.findAll(filters, pageInfo);
// System.out.println(list);
System.out.println(list.size()); assertThat(list).isNotNull();
}
> PageInfo.java에 getter 추가시켜줌 or lombok @Getter를 달아준다.
public int getListLimit() {
return listLimit;
}
@Getter
public class PageInfo {
private int currentPage;
private int pageLimit;
private int listCount;
private int listLimit;
...
다시 BoardDao.java로 넘어와서 코드작성해준다.
/*
* 기존에 Servlet/JSP에서는 쿼리문에서 rownum과 서브쿼리를 통해서 페이징 처리를 하였다.
* 하지만 Mybatis에서는 페이징 처리를 위해 RowBounds라는 클래스를 제공해준다.
* -> 쿼리문으로 조회되는 결과를 활용해서 RowBounds로 원하는 데이터만 가져옴
*
* RowBounds의 객체를 생성할 때 offset과 limit 값을 전달해서 페이징 처리를 쉽게 구현한다.
* (offset 만큼 건너 뛰고 limit만큼 가져온다.)
*
* offset = 0, limit = 10
* - 0개를 건너뛰고, 10개를 가져온다. (1번째 결과 ~ 10번째 결과까지)
* offset = 10, limit = 10
* - 10개를 건너뛰고, 10개를 가져온다. (11 ~ 20번째 데이터까지)
* offset = 20, limit = 10
* - 20개를 건너뛰고, 10개를 가져온다. (21 ~ 30번째 데이터까지)
*
*/
int offset = (pageInfo.getCurrentPage() - 1) * pageInfo.getListLimit();
int limit = pageInfo.getListLimit();
RowBounds rowBounds = new RowBounds(offset, limit);
return session.selectList("boardMapper.selectBoardListByFilters", map, rowBounds);
}
- 페이지 정보 가져올 수 있는 provider 생성
> BoardServiceTest.java
// 페이지 정보를 받아올 수 있는 Provider
public static Stream<Arguments> listProvider() {
// Stream 만들어서 리턴, 안에는 파라미터로 쓸 Arguments 리스트를 만들어서 넘겨줌
return Stream.of(
// 페이지번호, 게시글
Arguments.arguments((Object) new String[]{"B2", "B3"}, 1, 5),
Arguments.arguments((Object) new String[]{"B1"}, 1, 10),
Arguments.arguments((Object) new String[]{"B1"}, 16, 2)
);
}
좀 전에 작성했던 filterProvider 수정
@ParameterizedTest
@MethodSource("listProvider")
public void findAllTest(String[] filters, int currentPage, int expected) {
// 배열로 데이터 가져오기
// filters에는 B2, B3 값을 가지고 있다고 가정할 수 있음
// String[] filters = new String[] {"B2", "B3"};
// -> request.getParameterValues("filter");
List<Board> list = null;
PageInfo pageInfo = null;
int listCount = 0;
listCount = service.getBoardCount(filters);
// 페이지 info가져오기(현재 페이지, 페이징 몇개 보여줄지, 전체 게시글의 수, 한 페이지에서 보여줄 수)
pageInfo = new PageInfo(currentPage, 10, listCount, 10);
// 리스트 얻어오기 (from. service 객체)
list = service.findAll(filters, pageInfo);
assertThat(list).isNotNull();
assertThat(list.size()).isPositive().isEqualTo(expected);
}
- 게시글 상세 조회
댓글에 대한 클래스를 model.vo 밑에 생성해준다.
그리고 이전에 mvc2 할때 썼던 클래스문 가져오기
package com.kh.mybatis.board.model.vo;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Reply {
private int no;
private int boardNo;
private int writerNo;
private String writerId;
private String content;
private Date createDate;
private Date modifyDate;
}
Date 부분 넣을 때는 java.util에서 넣어주기
BoardServiceTest.java에 테스트 메소드 작성
//게시글 상세 데이터 조회를 위한 테스트 메소드 생성
public void findBoardByNoTest() {
// Board 객체 조회(service에서 findBoardByNo()메소드를 통해서)
Board board = null;
// 보드 조회 (내 DB에 있는 보드 확인)
board = service.findBoardByNo(157);
// 검증
assertThat(board).isNotNull();
}
findBoardByNoTest에서 발생하는 에러 없애주기 (ctrl+1)
-> BoardService.java에 메소드 생성 후 Dao를 통해서 가져올 객체를 만들어준다.
public Board findBoardByNo(int no) {
// 단건 조회
Board board = null;
SqlSession session = getSession();
// board객체는 Dao를 통해서 가져옴
board = dao.findBoardByNo(session, no);
session.close();
return board;
}
그 후 findBoardByNoTest에서 발생하는 에러 없애주기
BoardDao.java에도 메소드 생성
BoardDao.java
리턴은 나중에 수정
주석으로 막아뒀던 List<Reply> replies; 해제 시켜주고
java.util안에 있는 List를 import시켜준다. (같은 패키지 내에 있어서 import 필요 없다.)
- 기본적인 세팅 끝
다시 BoardDao.java로 와서 return값 수정
public Board findBoardByNo(SqlSession session, int no) {
return session.selectOne("boardMapper.selectBoardByNo", no);
}
> board-mapper.xml
<!-- 게시글 조회 쿼리문 -->
<select id="selectBoardByNo" parameterType="_int" resultMap="boardDetailResultMap">
<include refid="boardListSql" />
<!-- B.NO가 파라미터로 전달되는 no와 같은 것만 조회 -->
AND B.NO = #{no}
</select>
그리고 BoardServiceTest.java 에서 no값 일치하는 게시글 조회할 수 있게 수정
//게시글 상세 데이터 조회를 위한 테스트 메소드 생성
public void findBoardByNoTest() {
// Board 객체 조회(service에서 findBoardByNo()메소드를 통해서)
Board board = null;
// 보드 조회 (내 DB에 있는 보드 확인)
board = service.findBoardByNo(157);
// 검증 (no 값이 157과 동일한지 테스트 확인)
assertThat(board).isNotNull().extracting("no").isEqualTo(157);
}
- 댓글 포함해서 가져오기
Board.java에서의 Reply는 댓글 객체를 요소로 가지는 list로 가져오게 된다.
-> 조회되는 결과가 있으면 List객체에 요소를 담아주고, 없으면 빈 list객체를 가져온다.
BoardServiceTest.java에 assertThat 추가시켜주고 테스트 진행시 통과하지 않게 됨
-> BoardServiceTest에서 해줄 건 없고, board-mapper.xml에서 작업해준다.
방법 1) 쿼리문 각각 생성 (댓글, 게시글 쿼리문 따로) 후 하나의 ResultMap에서 mapping 작업
방법 2) 하나의 쿼리문으로 ResultMap에서 mapping 작업
config파일에서 댓글 데이터 타입에 대한 별칭을 부여해준다.
> board-mapper.xml
<!--
게시글 상세 보기 (댓글 포함)
1. 쿼리문을 각각 만들어서 하나의 ResultMap에서 매핑하는 방법
2. 하나의 쿼리문을 만들어서 하나의 ResultMap에서 매핑하는 방법
-->
<!-- 게시글 조회 쿼리문 -->
<select id="selectBoardByNo" parameterType="_int" resultMap="boardListResultMap">
<include refid="boardListSql" />
<!-- B.NO가 파라미터로 전달되는 no와 같은 것만 조회 -->
AND B.NO = #{no}
</select>
<!-- 댓글 조회 쿼리문 -->
<select id="selectRepliesByBoardNo" parameterType="_int" resultType="Reply">
<!-- 객체 조회 -->
SELECT R.NO,
R.BOARD_NO,
R.CONTENT,
M.ID,
R.CREATE_DATE,
R.MODIFY_DATE
FROM REPLY R
JOIN MEMBER M ON (R.WRITER_NO = M.NO)
WHERE R.STATUS = 'Y' AND BOARD_NO=#{boardNo}
ORDER BY R.NO DESC
</select>
selectRepliesByBoardNo의 resultType을 만들어준 별칭 Reply로 바꿔준다.
게시글 조회 쿼리문의 resultMap을 boardDetailResultMap라는 이름으로 수정해준 뒤 새로운 resultMap을 위쪽에 만들어준다.
<!-- 위에서 작성한 ResultMap을 extends를 활용해서 상속 후 확장하여 사용해준다. -->
<!--
1. 쿼리문을 각각 만들어서 하나의 ResultMap에서 매핑하는 방법
- extends 속성 : 다른 resultMap을 상속하는 속성이다.
- collection : collection 태그는 컬렉션에 해당하는 필드에 조회 결과를 매핑할 때 사용한다.
: replies(Board.java)라는 필드가 collection으로 되어있어서 여기에 객체를 매핑하기 위한 태그
- javaType 속성 : 어떤 자바 컬렉션 타입인지 명시해준다.
- select 속성 : 컬렉션을 조회하고자 하는 쿼리문의 ID를 적어준다.
: 댓글 쿼리문 Id명
- column 속성 : 컬렉션을 조회하고자 하는 쿼리문에 파라미터를 전달할 때 사용한다.
* 1:1 관계인 객체를 조회해 오고 싶다면 association 태그를 사용해서 매핑해주면 된다.
<resultMap type="Board" extends="boardListResultMap" id="boardDetailResultMap">
<collection property="replies" javaType="arraylist" select="selectRepliesByBoardNo" column="NO" />
<!-- <association property="reply" javaType="Reply" select="..." column=".." /> -->
</resultMap>
조회된 값을 보면 댓글에 대한 객체가 list가 만들어져서 들어가는데 mapping이 되어있지 않은 것들이 있음
-> why? 조회하는 결과의 컬럼명과 매핑하려는 객체의 필드명과 다르기 때문
-> resultMap 만들어서 mapping 해주면 된다.
resultType -> resultMap으로 수정 후 replyResultMap으로 수정해줌
그리고 수정한 replyResultMap으로 새로운 resultMap 생성해준다.
<!-- Reply에서 매핑 안되는 부분들 mapping 시켜줌 -->
<resultMap type="Reply" id="replyResultMap">
<id property="no" column="NO"/>
<result property="boardNo" column="BOARD_NO"/>
<result property="writerId" column="ID"/>
<result property="content" column="CONTENT"/>
<result property="createDate" column="CREATE_DATE"/>
<result property="modifyDate" column="MODIFY_DATE"/>
</resultMap>
방법2) 하나의 쿼리문을 만들어서 ResultMap에서 매핑하는 방법
<!-- 2. 하나의 쿼리문을 만들어서 하나의 ResultMap에서 매핑하는 방법 -->
<select id="selectBoardByNo" parameterType="_int" resultMap="boardDetailResultMap">
SELECT B.NO AS B_NO,
B.TITLE AS B_TITLE,
M.ID AS B_ID,
B.READCOUNT AS B_READCOUNT,
B.ORIGINAL_FILENAME AS B_ORIGINAL_FILENAME,
B.RENAMED_FILENAME AS B_RENAMED_FILENAME,
B.CONTENT AS B_CONTENT,
B.TYPE AS B_TYPE,
B.CREATE_DATE AS B_CREATE_DATE,
B.MODIFY_DATE AS B_MODIFY_DATE,
R.NO AS R_NO,
R.BOARD_NO AS R_BOARD_NO,
R.CONTENT AS R_CONTENT,
M2.ID AS R_ID,
R.CREATE_DATE AS R_CREATE_DATE,
R.MODIFY_DATE AS R_MODIFY_DATE
FROM BOARD B
JOIN MEMBER M ON(B.WRITER_NO = M.NO)
LEFT OUTER JOIN REPLY R ON(B.NO = R.BOARD_NO)
LEFT OUTER JOIN MEMBER M2 ON(R.WRITER_NO = M2.NO)
WHERE B.STATUS = 'Y' AND B.NO = #{no}
</select>
SELECT 쿼리문을 위처럼 만들어 준 뒤 resultMap을 만들어준다.
<!--
2. 하나의 쿼리문을 만들어서 하나의 ResultMap에서 매핑하는 방법
<resultMap type="Board" id="boardDetailResultMap">
<id property="no" column=""B_NO/>
<result property="title" column="B_TITLE"/>
<result property="writerId" column="B_ID"/>
<result property="readCount" column="B_READCOUNT"/>
<result property="originalFileName" column="B_ORIGINAL_FILENAME"/>
<result property="renamedFileName" column="B_RENAMED_FILENAME"/>
<result property="content" column="B_CONTENT"/>
<result property="type" column="B_TYPE"/>
<result property="createDate" column="B_CREATE_DATE"/>
<result property="modifyDate" column="B_MODIFY_DATE"/>
<!--
- 댓글에 대한 list를 받아와야 하기 때문에 collection을 추가해준다. (여러 개 값 조회)
- 댓글에 대한 resultMap을 만들어줘야 하기 때문에 replyResultMap을 새로 만들어 준다.
- B로 시작하는 애들은 한 번만 매핑이 되고, R로 시작하는 애들은 Reply객체로 매핑 후,
얘네를 담고 있는 arraylist로 만들어서 replies로 담아준다. 그 때 참고하는 것이 replyResultMap이다.
-->
<collection property="replies" javaType="arraylist" resultMap="replyResultMap" />
</resultMap>
<!-- 댓글에 대한 새로운 ResultMap, mapping 작업 -->
<resultMap type="Reply" id="replyResultMap">
<id property="no" column="R_NO"/>
<result property="boardNo" column="R_BOARD_NO"/>
<result property="writerId" column="R_ID"/>
<result property="content" column="R_CONTENT"/>
<result property="createDate" column="R_CREATE_DATE"/>
<result property="modifyDate" column="R_MODIFY_DATE"/>
</resultMap>
-->
extends(상속) 적용하려는 경우
<resultMap type="Board" id="boardListResultMap">
<id property="no" column="NO"/>
<result property="title" column="TITLE"/>
<result property="writerId" column="ID"/>
<result property="readCount" column="READCOUNT"/>
<result property="originalFileName" column="ORIGINAL_FILENAME"/>
<result property="renamedFileName" column="RENAMED_FILENAME"/>
<result property="content" column="CONTENT"/>
<result property="type" column="TYPE"/>
<result property="createDate" column="CREATE_DATE"/>
<result property="modifyDate" column="MODIFY_DATE"/>
</resultMap>
<!-- 댓글에 대한 새로운 ResultMap, mapping 작업 -->
<resultMap type="Reply" id="replyResultMap">
<id property="no" column="NO"/>
<result property="boardNo" column="BOARD_NO"/>
<result property="writerId" column="ID"/>
<result property="content" column="CONTENT"/>
<result property="createDate" column="CREATE_DATE"/>
<result property="modifyDate" column="MODIFY_DATE"/>
</resultMap>
기존에 있었던 Board 아래에 댓글에 대한 mapping 작업해줌
그 다음 boardListResultMap을 상속하는 resultMap을 만들어준다.
<!-- 상속 추가 후 기존 쿼리문 collection 제외 후 삭제 -->
<resultMap type="Board" extends="boardListResultMap" id="boardDetailResultMap">
<collection property="replies" javaType="arraylist" columnPrefix="R_" resultMap="replyResultMap" />
</resultMap>
방법 1을 쓸 경우
장점 : 관리 편리, 쿼리문 수정 시 편하게 수정 가능
단점 : select 두 번 수행함
방법 2를 쓸 경우
장점 : select 한 번 수행
단점 : 쿼리문이 어려움 -> 유지보수 어려움
테스트 코드 수정
BoardServiceTest.java
@ParameterizedTest @ValueSource(ints= {156, 157})
//게시글 상세 데이터 조회를 위한 테스트 메소드 생성
public void findBoardByNoTest(int no) {
// Board 객체 조회(service에서 findBoardByNo()메소드를 통해서)
Board board = null;
// 보드 조회 (내 DB에 있는 보드 확인)
board = service.findBoardByNo(no);
// 검증 (no 값이 157과 동일한지 테스트 확인)
assertThat(board).isNotNull().extracting("no").isEqualTo(no);
assertThat(board.getReplies()).isNotNull();
assertThat(board.getReplies().size()).isNotNull().isGreaterThan(0);
}
전체 코드
> mybatis-config.xml
...
<!-- typeAliases : Mybatis에서 사용하는 데이터 타입에 별칭 부여 -->
<!-- VO/DTO 객체의 타입에 별칭을 선언하는 영역 -->
<typeAliases>
<typeAlias type="com.kh.mybatis.member.model.vo.Member" alias="Member"/>
<typeAlias type="com.kh.mybatis.board.model.vo.Board" alias="Board"/>
<typeAlias type="com.kh.mybatis.board.model.vo.Reply" alias="Reply"/>
</typeAliases>
...
> BoardServiceTest.java
package com.kh.mybatis.board.model.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import com.kh.mybatis.board.model.vo.Board;
import com.kh.mybatis.common.util.PageInfo;
class BoardServiceTest {
private BoardService service;
@BeforeEach
public void setUp() throws Exception {
// @BeforeEach : 아래 테스트 케이스들 작성 시 한번씩 수행
// 그럴때마다 새로운 service 객체를 생성 후 넘겨주기
service = new BoardService();
}
@Test
@Disabled
public void nothing() {
}
@Test
@Disabled
public void create() {
// 정상적으로 setUp()에서 객체가 만들어져서 대입이 됐는지 확인 후 @Disabled
assertThat(service).isNotNull();
}
@ParameterizedTest
@CsvSource(
value = {
"is, null, null, 1",
"null, 22, null, 2",
"null, null, 테스트, 1",
"null, null, null, 157"},
nullValues = "null"
)
// Board 객체를 요소로 담고있는 list를 반환해주는 함수
public void findAllTest(String writer, String title, String content, int expected) {
List<Board> list = null;
list = service.findAll(writer, title, content);
// 각각 파라미터를 받는 것 외에는 Null 처리를 한다고 가정
// title을 받을 때 writer, content는 null로 출력
// 전부 null일 경우 다중<if>구문 에서 그다음에 있는 B.STATUS='Y'로 넘어가서 전체 데이터 조회된다.
// String writer = null;
// String title = null;
// String content = "테스트";
// findAllTest()의 파라미터 값으로 보내준 뒤 없애도 된다.
assertThat(list).isNotNull();
assertThat(list.size()).isPositive().isEqualTo(expected);
}
@ParameterizedTest
@MethodSource("filterProvider")
public void getBoardCountTest(String[] filters) {
int count = 0;
// 현재 게시글의 갯수 가져오기
count = service.getBoardCount(filters);
System.out.println(count);
// 게시글의 숫자는 양수이면서, 0보다 크다는 것이라고 가정
assertThat(count).isPositive().isGreaterThan(0);
}
@ParameterizedTest
@MethodSource("listProvider")
public void findAllTest(String[] filters, int currentPage, int expected) {
// 배열로 데이터 가져오기
// filters에는 B2, B3 값을 가지고 있다고 가정할 수 있음
// String[] filters = new String[] {"B2", "B3"};
// -> request.getParameterValues("filter");
List<Board> list = null;
PageInfo pageInfo = null;
int listCount = 0;
listCount = service.getBoardCount(filters);
// 페이지 info가져오기(현재 페이지, 페이징 몇개 보여줄지, 전체 게시글의 수, 한 페이지에서 보여줄 수)
pageInfo = new PageInfo(currentPage, 10, listCount, 10);
// 리스트 얻어오기 (from. service 객체)
list = service.findAll(filters, pageInfo);
assertThat(list).isNotNull();
assertThat(list.size()).isPositive().isEqualTo(expected);
}
@ParameterizedTest
@ValueSource(ints= {156, 157})
//게시글 상세 데이터 조회를 위한 테스트 메소드 생성
public void findBoardByNoTest(int no) {
// Board 객체 조회(service에서 findBoardByNo()메소드를 통해서)
Board board = null;
// 보드 조회 (내 DB에 있는 보드 확인)
board = service.findBoardByNo(no);
// 검증 (no 값이 157과 동일한지 테스트 확인)
assertThat(board).isNotNull().extracting("no").isEqualTo(no);
assertThat(board.getReplies()).isNotNull();
assertThat(board.getReplies().size()).isNotNull().isGreaterThan(0);
}
public static Stream<Arguments> filterProvider() {
// Stream 만들어서 리턴, 안에는 파라미터로 쓸 Arguments 리스트를 만들어서 넘겨줌
return Stream.of(
Arguments.arguments((Object) new String[]{"B2", "B3"}),
Arguments.arguments((Object) new String[]{"B1"})
);
}
// 페이지 정보를 받아올 수 있는 Provider
public static Stream<Arguments> listProvider() {
// Stream 만들어서 리턴, 안에는 파라미터로 쓸 Arguments 리스트를 만들어서 넘겨줌
return Stream.of(
// 페이지번호, 게시글
Arguments.arguments((Object) new String[]{"B2", "B3"}, 1, 5),
Arguments.arguments((Object) new String[]{"B1"}, 1, 10),
Arguments.arguments((Object) new String[]{"B1"}, 16, 2)
);
}
}
> board-mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Mapper 설정 파일임을 선언 -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="boardMapper">
<sql id="boardListSql">
SELECT B.NO,
B.TITLE,
M.ID,
B.READCOUNT,
B.ORIGINAL_FILENAME,
B.RENAMED_FILENAME,
B.CONTENT,
B.TYPE,
B.CREATE_DATE,
B.MODIFY_DATE
FROM BOARD B
JOIN MEMBER M ON(B.WRITER_NO = M.NO)
WHERE B.STATUS = 'Y'
</sql>
<resultMap type="Board" id="boardListResultMap">
<id property="no" column="NO"/>
<result property="title" column="TITLE"/>
<result property="writerId" column="ID"/>
<result property="readCount" column="READCOUNT"/>
<result property="originalFileName" column="ORIGINAL_FILENAME"/>
<result property="renamedFileName" column="RENAMED_FILENAME"/>
<result property="content" column="CONTENT"/>
<result property="type" column="TYPE"/>
<result property="createDate" column="CREATE_DATE"/>
<result property="modifyDate" column="MODIFY_DATE"/>
</resultMap>
<!-- 댓글에 대한 새로운 ResultMap, mapping 작업 -->
<resultMap type="Reply" id="replyResultMap">
<id property="no" column="NO"/>
<result property="boardNo" column="BOARD_NO"/>
<result property="writerId" column="ID"/>
<result property="content" column="CONTENT"/>
<result property="createDate" column="CREATE_DATE"/>
<result property="modifyDate" column="MODIFY_DATE"/>
</resultMap>
<!-- 위에서 작성한 ResultMap을 extends를 활용해서 상속 후 확장하여 사용해준다. -->
<!--
1. 쿼리문을 각각 만들어서 하나의 ResultMap에서 매핑하는 방법
- extends 속성 : 다른 resultMap을 상속하는 속성이다.
- collection : collection 태그는 컬렉션에 해당하는 필드에 조회 결과를 매핑할 때 사용한다.
: replies(Board.java)라는 필드가 collection으로 되어있어서 여기에 객체를 매핑하기 위한 태그
- javaType 속성 : 어떤 자바 컬렉션 타입인지 명시해준다.
- select 속성 : 컬렉션을 조회하고자 하는 쿼리문의 ID를 적어준다.
: 댓글 쿼리문 Id명
- column 속성 : 컬렉션을 조회하고자 하는 쿼리문에 파라미터를 전달할 때 사용한다.
* 1:1 관계인 객체를 조회해 오고 싶다면 association 태그를 사용해서 매핑해주면 된다.
<resultMap type="Board" extends="boardListResultMap" id="boardDetailResultMap">
<collection property="replies" javaType="arraylist" select="selectRepliesByBoardNo" column="NO" />
<!-- <association property="reply" javaType="Reply" select="..." column=".." /> -->
</resultMap>
<!-- Reply에서 매핑 안되는 부분들 mapping 시켜줌 -->
<resultMap type="Reply" id="replyResultMap">
<id property="no" column="NO"/>
<result property="boardNo" column="BOARD_NO"/>
<result property="writerId" column="ID"/>
<result property="content" column="CONTENT"/>
<result property="createDate" column="CREATE_DATE"/>
<result property="modifyDate" column="MODIFY_DATE"/>
</resultMap>
-->
<!--
2. 하나의 쿼리문을 만들어서 하나의 ResultMap에서 매핑하는 방법
<resultMap type="Board" id="boardDetailResultMap">
<id property="no" column=""B_NO/>
<result property="title" column="B_TITLE"/>
<result property="writerId" column="B_ID"/>
<result property="readCount" column="B_READCOUNT"/>
<result property="originalFileName" column="B_ORIGINAL_FILENAME"/>
<result property="renamedFileName" column="B_RENAMED_FILENAME"/>
<result property="content" column="B_CONTENT"/>
<result property="type" column="B_TYPE"/>
<result property="createDate" column="B_CREATE_DATE"/>
<result property="modifyDate" column="B_MODIFY_DATE"/>
<!--
- 댓글에 대한 list를 받아와야 하기 때문에 collection을 추가해준다. (여러 개 값 조회)
- 댓글에 대한 resultMap을 만들어줘야 하기 때문에 replyResultMap을 새로 만들어 준다.
- B로 시작하는 애들은 한 번만 매핑이 되고, R로 시작하는 애들은 Reply객체로 매핑 후,
얘네를 담고 있는 arraylist로 만들어서 replies로 담아준다. 그 때 참고하는 것이 replyResultMap이다.
-->
<collection property="replies" javaType="arraylist" resultMap="replyResultMap" />
</resultMap>
<!-- 댓글에 대한 새로운 ResultMap, mapping 작업 -->
<resultMap type="Reply" id="replyResultMap">
<id property="no" column="R_NO"/>
<result property="boardNo" column="R_BOARD_NO"/>
<result property="writerId" column="R_ID"/>
<result property="content" column="R_CONTENT"/>
<result property="createDate" column="R_CREATE_DATE"/>
<result property="modifyDate" column="R_MODIFY_DATE"/>
</resultMap>
-->
<!-- 상속 추가 후 기존 쿼리문 collection 제외 후 삭제 -->
<resultMap type="Board" extends="boardListResultMap" id="boardDetailResultMap">
<collection property="replies" javaType="arraylist" columnPrefix="R_" resultMap="replyResultMap" />
</resultMap>
<select id="selectAll" parameterType="map" resultType="boardListResultMap">
<!-- 1. 다중 <if>를 활용한 검색 기능 구현
SELECT B.NO,
B.TITLE,
M.ID,
B.READCOUNT,
B.ORIGINAL_FILENAME,
B.RENAMED_FILENAME,
B.CONTENT,
B.CREATE_DATE,
B.MODIFY_DATE
FROM BOARD B
JOIN MEMBER M ON(B.WRITER_NO = M.NO)
WHERE B.STATUS = 'Y'
<if test="writer != null">
<!-- writer가 Null이 아니면 아래 쿼리문 추가 시키는 구문, null이면 동작하지 않음 -->
<!-- 검색하는 내용만 포함되면 조회가능 -->
AND M.ID LIKE '%' || #{writer} || '%'
</if>
<if test="title != null">
<!-- title 값이 Null이 아니면 아래 쿼리문 추가 시키는 구문, null이면 동작하지 않음 -->
AND B.TITLE LIKE '%' || #{title} || '%'
</if>
<if test="content != null">
<!-- content 값이 Null이 아니면 아래 쿼리문 추가 시키는 구문, null이면 동작하지 않음 -->
AND B.CONTENT LIKE '%' || #{content} || '%'
</if>
-->
<!--
2. <where>와 다중 <if>를 활용한 검색 기능 구현
SELECT B.NO,
B.TITLE,
M.ID,
B.READCOUNT,
B.ORIGINAL_FILENAME,
B.RENAMED_FILENAME,
B.CONTENT,
B.CREATE_DATE,
B.MODIFY_DATE
FROM BOARD B
JOIN MEMBER M ON(B.WRITER_NO = M.NO)
<where>
<if test="writer != null">
M.ID LIKE '%' || #{writer} || '%'
</if>
<if test="title != null">
AND B.TITLE LIKE '%' || #{title} || '%'
</if>
<if test="content != null">
AND B.CONTENT LIKE '%' || #{content} || '%'
</if>
AND B.STATUS = 'Y'
</where>
-->
<!--
3.<trim>과 다중<if>를 활용한 검색 기능 구현
SELECT B.NO,
B.TITLE,
M.ID,
B.READCOUNT,
B.ORIGINAL_FILENAME,
B.RENAMED_FILENAME,
B.CONTENT,
B.CREATE_DATE,
B.MODIFY_DATE
FROM BOARD B
JOIN MEMBER M ON(B.WRITER_NO = M.NO)
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="writer != null">
M.ID LIKE '%' || #{writer} || '%'
</if>
<if test="title != null">
AND B.TITLE LIKE '%' || #{title} || '%'
</if>
<if test="content != null">
AND B.CONTENT LIKE '%' || #{content} || '%'
</if>
AND B.STATUS = 'Y'
</trim>
-->
<!--
4.<choose>, <when>, <otherwise>를 활용한 검색 기능 구현
-->
<include refid="boardListSql" />
<choose>
<when test="writer != null">
<!-- writer가 있으면 넘겨주는 값으로 해당하는 데이터만 조회되게 해준다. -->
AND M.ID LIKE '%' || #{writer} || '%'
</when>
<when test="title != null">
AND B.TITLE LIKE '%' || #{title} || '%'
</when>
<when test="content != null">
AND B.CONTENT LIKE '%' || #{content} || '%'
</when>
<!--
<otherwise>
위의 조건 중 하나도 만족하지 않는 경우 포함될 쿼리 작성
</otherwise>
-->
</choose>
</select>
<select id="selectBoardListByFilters" parameterType="map" resultMap="boardListResultMap">
<include refid="boardListSql" />
<!-- filters가 null인지 확인 후 null이 아니면If내의 구문 실행 -->
<if test="filters != null">
<!--
AND B.TYPE IN ('B2', 'B3')
위 결과를 만들기 위해서 foreach 태그를 사용한다.
- collection : 파라미터로 넘어온 배열, 리스트를 지정한다.
- item : 배열, 리스트의 각 요소들의 값이 들어가는 변수이다.
- index : 반복 횟수 (0부터 시작한다.)
- open : 반복문 시작 전 출력할 문자열을 지정한다.
- close : 반복문 종료 전에 출력할 문자열을 지정한다.
- separator : 반복할 때마다 반복을 구분할 구분자를 지정한다.
-->
AND B.TYPE IN
<foreach collection="filters" item="filter" open="(" separator="," close=")">
#{filter}
</foreach>
</if>
</select>
<select id="getBoardCountByFilters" parameterType="map" resultType="_int">
SELECT COUNT(*)
FROM BOARD
WHERE STATUS = 'Y'
AND TYPE IN ('B2', 'B3')
<if test="list != null">
AND B.TYPE IN
<foreach collection="filters" item="filter" open="(" separator="," close=")">
#{filter}
</foreach>
</if>
</select>
<!--
게시글 상세 보기 (댓글 포함)
1. 쿼리문을 각각 만들어서 하나의 ResultMap에서 매핑하는 방법
<!-- 게시글 조회 쿼리문 -->
<select id="selectBoardByNo" parameterType="_int" resultMap="boardDetailResultMap">
<include refid="boardListSql" />
<!-- B.NO가 파라미터로 전달되는 no와 같은 것만 조회 -->
AND B.NO = #{no}
</select>
<!-- 댓글 조회 쿼리문 -->
<select id="selectRepliesByBoardNo" parameterType="_int" resultMap="replyResultMap">
<!-- 객체 조회 -->
SELECT R.NO,
R.BOARD_NO,
R.CONTENT,
M.ID,
R.CREATE_DATE,
R.MODIFY_DATE
FROM REPLY R
JOIN MEMBER M ON (R.WRITER_NO = M.NO)
WHERE R.STATUS = 'Y' AND BOARD_NO=#{boardNo}
ORDER BY R.NO DESC
</select>
-->
<!--
2. 하나의 쿼리문을 만들어서 하나의 ResultMap에서 매핑하는 방법
- B.(보드 관련 컬럼들), R.(댓글 관련 컬럼들)
-->
<select id="selectBoardByNo" parameterType="_int" resultMap="boardDetailResultMap">
SELECT B.NO,
B.TITLE,
M.ID AS,
B.READCOUNT,
B.ORIGINAL_FILENAME,
B.RENAMED_FILENAME,
B.CONTENT,
B.TYPE,
B.CREATE_DATE,
B.MODIFY_DATE,
R.NO AS R_NO,
R.BOARD_NO AS R_BOARD_NO,
R.CONTENT AS R_CONTENT,
M2.ID AS R_ID,
R.CREATE_DATE AS R_CREATE_DATE,
R.MODIFY_DATE AS R_MODIFY_DATE
FROM BOARD B
JOIN MEMBER M ON(B.WRITER_NO = M.NO)
LEFT OUTER JOIN REPLY R ON(B.NO = R.BOARD_NO)
LEFT OUTER JOIN MEMBER M2 ON(R.WRITER_NO = M2.NO)
WHERE B.STATUS = 'Y' AND B.NO = #{no}
</select>
</mapper>
> BoardDao.java
package com.kh.mybatis.board.model.dao;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import com.kh.mybatis.board.model.vo.Board;
import com.kh.mybatis.common.util.PageInfo;
public class BoardDao {
public int getBoardCount(SqlSession session, List<String> filters) {
return session.selectOne("boardMapper.getBoardCountByFilters", filters);
}
public List<Board> findAll(SqlSession session, String writer, String title, String content) {
// map으로 여러 개의 키 값을 담아줄 수 있다.
Map<String, String> map = new HashMap<>();
map.put("writer", writer);
map.put("title", title);
map.put("content", content);
return session.selectList("boardMapper.selectAll", map);
}
public List<Board> findAll(SqlSession session, String[] filters, PageInfo pageInfo) {
/*
* List 타입이나 Array 타입의 데이터를 파라미터로 전달하면 내부적으로는 Map으로 타입이 변환되어서 저장되기 때문에
* Mapper에서는 list(또는 collection)나 array라는 이름으로 사용해야 한다.
*
* Dao.java
* session.selectList("boardMapper.selectBoardListByFilters", filters);
*
* Mapper.xml
* <if test="array != null">
* ...
* </if>
*
* 만약에 filters라는 이름을 Mapper에서 사용하고 싶으면 Map 객체에 담아서 파라미터로 전달한다.(아래 내용)
*/
Map<String, Object> map = new HashMap<>();
map.put("filters", filters);
/*
* 기존에 Servlet/JSP에서는 쿼리문에서 rownum과 서브쿼리를 통해서 페이징 처리를 하였다.
* 하지만 Mybatis에서는 페이징 처리를 위해 RowBounds라는 클래스를 제공해준다.
* -> 쿼리문으로 조회되는 결과를 활용해서 RowBounds로 원하는 데이터만 가져옴
*
* RowBounds의 객체를 생성할 때 offset과 limit 값을 전달해서 페이징 처리를 쉽게 구현한다.
* (offset 만큼 건너 뛰고 limit만큼 가져온다.)
*
* offset = 0, limit = 10
* - 0개를 건너뛰고, 10개를 가져온다. (1번째 결과 ~ 10번째 결과까지)
* offset = 10, limit = 10
* - 10개를 건너뛰고, 10개를 가져온다. (11 ~ 20번째 데이터까지)
* offset = 20, limit = 10
* - 20개를 건너뛰고, 10개를 가져온다. (21 ~ 30번째 데이터까지)
*
*/
int offset = (pageInfo.getCurrentPage() - 1) * pageInfo.getListLimit();
int limit = pageInfo.getListLimit();
RowBounds rowBounds = new RowBounds(offset, limit);
return session.selectList("boardMapper.selectBoardListByFilters", map, rowBounds);
}
public Board findBoardByNo(SqlSession session, int no) {
return session.selectOne("boardMapper.selectBoardByNo", no);
}
}
> Board.java
package com.kh.mybatis.board.model.vo;
import java.util.Date;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Board {
private int no;
private int rowNum;
private int writerNo;
private String writerId;
private String title;
private String content;
private String type;
private String originalFileName;
private String renamedFileName;
private int readCount;
private String status;
private List<Reply> replies;
private Date createDate;
private Date modifyDate;
}
> Reply.java
package com.kh.mybatis.board.model.vo;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Reply {
private int no;
private int boardNo;
private int writerNo;
private String writerId;
private String content;
private Date createDate;
private Date modifyDate;
}
> BoardService.java
package com.kh.mybatis.board.model.service;
import java.util.Arrays;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import com.kh.mybatis.board.model.dao.BoardDao;
import com.kh.mybatis.board.model.vo.Board;
import com.kh.mybatis.common.util.PageInfo;
import static com.kh.mybatis.common.template.SqlSessionTemplate.getSession;
public class BoardService {
// Dao 객체에게 데이터 조회해달라고 요청
private BoardDao dao = new BoardDao();
public int getBoardCount(String[] filters) {
// count라는 값을 반환
int count = 0;
SqlSession session = getSession();
// dao에서 게시글 갯수 구해오기
count = dao.getBoardCount(session, Arrays.asList(filters));
// 조회되는 값이 있으면 session close
session.close();
return count;
}
public List<Board> findAll(String writer, String title, String content) {
// Board객체를 가지는 list를 만들겠다- 선언
List<Board> list = null;
SqlSession session = getSession();
// dao에게서 리스트를 받을 것임
list = dao.findAll(session, writer, title, content);
session.close();
return list;
}
public List<Board> findAll(String[] filters,PageInfo pageInfo) {
List<Board> list = null;
SqlSession session = getSession();
// dao에게 findAll 요청 하면서 매개값 session, filter 넘겨줌 (추가로 pageInfo도)
list = dao.findAll(session, filters, pageInfo);
return list;
}
public Board findBoardByNo(int no) {
// 단건 조회
Board board = null;
SqlSession session = getSession();
// board객체는 Dao를 통해서 가져옴
board = dao.findBoardByNo(session, no);
session.close();
return board;
}
}
> PageInfo.java
package com.kh.mybatis.common.util;
import lombok.Getter;
@Getter
public class PageInfo {
private int currentPage;
private int pageLimit;
private int listCount;
private int listLimit;
/**
* @param currentPage 현재 페이지
* @param pageLimit 한 페이지에 보이는 페이지의 수
* @param listCount 전체 리스트의 수
* @param listLimit 한 페이지에 표시될 리스트의 수
*/
public PageInfo(int currentPage, int pageLimit, int listCount, int listLimit) {
this.currentPage = currentPage;
this.pageLimit = pageLimit;
this.listCount = listCount;
this.listLimit = listLimit;
}
/**
* @return 전체 페이지 중 가장 마지막 페이지
*/
public int getMaxPage() {
/*
listCount = 100, listLimit = 10
100 / 10 = 10.0 => 10페이지
101 / 10 = 10.1 => 11페이지
103 / 10 = 10.3 => 11페이지
109 / 10 = 10.9 => 11페이지
110 / 10 = 11.0 => 11페이지
111 / 10 = 11.1 => 12페이지
*/
return (int) Math.ceil((double) this.listCount / this.listLimit);
}
/**
*
* @return 페이징 된 페이지 중 시작 페이지
*/
public int getStartPage() {
/*
< 1 2 3 4 5 6 7 8 9 10 >
< 11 12 13 14 15 16 17 18 19 20 >
< 21 22 23 24 25 26 27 28 29 30 >
1, 11, 21, 31, .... => (10 * n) + 1 (n >= 0)
1 ~ 10 : n = 0
11 ~ 20 : n = 1
21 ~ 30 : n = 2
31 ~ 40 : n = 3
....
n = (currentPage - 1) / pageLimit
(10 * ((currentPage - 1) / pageLimit)) + 1 (n >= 0)
*/
return (this.pageLimit * ((this.currentPage - 1) / this.pageLimit)) + 1;
}
/**
*
* @return 페이징 된 페이지 중 마지막 페이지
*/
public int getEndPage() {
// 10, 20, 30, 40, ....
int endPage = this.getStartPage() + this.pageLimit - 1;
return endPage > this.getMaxPage() ? this.getMaxPage() : endPage;
}
/**
*
* @return 현재 페이지
*/
public int getCurrentPage() {
return this.currentPage;
}
/**
*
* @return 이전 페이지
*/
public int getPrevPage() {
int prevPage = this.getCurrentPage() - 1;
return prevPage < 1 ? 1 : prevPage;
}
/**
*
* @return 다음 페이지
*/
public int getNextPage() {
int nextPage = this.getCurrentPage() + 1;
return nextPage > this.getMaxPage() ? this.getMaxPage() : nextPage;
}
/**
*
* @return 페이지의 시작 리스트
*/
public int getStartList() {
return (this.getCurrentPage() - 1) * this.listLimit + 1;
}
/**
*
* @return 페이지의 마지막 리스트
*/
public int getEndList() {
int endList = this.getStartList() + this.listLimit - 1;
return endList > this.listCount ? this.listCount : endList;
}
}
'국비지원_Java > Java Programming_2' 카테고리의 다른 글
자바 프로그래밍_Day_100_Framework : MyBatis 정리 / log4j (0) | 2022.01.19 |
---|---|
자바 프로그래밍_Day_99_MyBatis 게시글 등록 / 수정 / 삭제 (0) | 2022.01.16 |
자바 프로그래밍_Day_97_Framework : MyBatis 동적 SQL (0) | 2022.01.08 |
자바 프로그래밍_Day_96_Framework : MyBatis / 동적 쿼리 (0) | 2022.01.06 |
자바 프로그래밍_Day_95_Framework:MyBatis (0) | 2022.01.04 |