일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- sql
- JavaScript 내장객체
- 생활코딩
- web
- sqldeveloper
- javascript
- PWA
- 서브쿼리
- springaop
- progressive web app
- tdd
- 메이븐
- mybatis
- 프레임워크
- Oracle
- 자바프로그래밍
- js
- 스프링
- 국비지원
- SpringMVC
- TodayILearned
- 오라클
- 자바스크립트
- framework
- CSS
- maven
- TIL
- 프로그레시브웹앱
- javaprogramming
- HTML
- Today
- Total
1cm
자바 프로그래밍_Day_99_MyBatis 게시글 등록 / 수정 / 삭제 본문
2022. 01. 06
MyBatis를 활용한 게시글 등록 / 수정 / 삭제
- 전 날 작업한 것들로 INSERT, UPDATE, DELETE 작업 진행
-> 기존 SAVE 메소드 기능
1) 데이터베이스 INSERT : 값이 존재하지 않을 경우 INSERT
2) 데이터베이스 UPDATE : 이미 존재하는 값일 경우 UPDATE
수업 제대로 진행하기 전에 각각의 테스트 메소드들에게 Display 어노테이션을 붙여줬다.
> 전체 : BoardService 테스트
> findAllTest (검색 기능 적용) : 게시글 목록 조회(검색 기능 적용) 테스트
> getBoardCountTest : 게시글 수 조회(필터 적용) 테스트
> findAllTest : 게시글 목록 조회(필터 적용) 테스트
> findBoardByNoTest : 게시글 상세 조회(댓글 포함) 테스트
그리고나서 게시글 등록 기능을 위한 테스트 메소드를 생성해줬다.
// 게시글 등록기능을 위한 테스트 생성
@Test
@Order(5)
@DisplayName("게시글 등록 테스트")
public void insertBoardTest() {
int result = 0;
Board board = new Board();
// member 테이블을 참조 하고 있기 때문에 MEMBER내의 pk값에 해당하는 값만 가져올 수 있다.
board.setWriterNo(3);
board.setTitle("mybatis 게시글");
board.setContent("mybatis로 게시글 등록을 해보았습니다.");
// service 객체에서 save 메소드를 사용
// 영향받은 행의 갯수(result) 반환됨
result = service.save(board);
// 정상적으로 insert 후의 검증 코드
assertThat(result).isGreaterThan(0);
}
save에서 에러나는 부분 -> BoardService에 메소드 생성해주기
> BoardService에 생성된 save(Board board) 메소드의 리턴값을 임의로 정수값 설정 후(0이상) 테스트 진행해보기
통과 한다면 save 부분 로직 만들기
public int save(Board board) {
int result = 0;
SqlSession session = getSession();
// service에서 직접 db에 접근 x -> DAO에게 넘겨줌
if(board.getNo() != 0) {
// UPDATE
} else {
// INSERT
result = dao.insertBoard(session, board);
}
// commit, rollback 작업
if (result > 0) {
session.commit();
} else {
session.rollback();
}
session.close();
return result;
}
insertBoard에서 발생하는 에러 -> BoardDao에 insertBoard 메소드 생성해준다.
그 다음 BoardDao에서 코드 작성
public int insertBoard(SqlSession session, Board board) {
return session.insert("boardMapper.insertBoard", board);
}
insertBoard라는 아이디를 가지는 쿼리문이 없어서 테스트는 통과하지 않을 것임.
-> board-mapper.xml에서 쿼리문 만들어주기
<!-- 게시글 생성 쿼리문 -->
<!-- 웬만하면 insert되는 컬럼을 명시해주는 것이 좋다.(select도 마찬가지) -->
<insert id="insertBoard">
INSERT INTO BOARD (
NO,
WRITER_NO,
TITLE,
CONTENT,
ORIGINAL_FILENAME,
RENAMED_FILENAME,
READCOUNT,
STATUS,
TYPE,
CREATE_DATE,
MODIFY_DATE
) VALUES (
SEQ_BOARD_NO.NEXTVAL,
#{writerNo},
#{title},
#{content},
#{originalFileName},
#{renamedFileName},
DEFAULT,
DEFAULT,
DEFAULT,
DEFAULT,
DEFAULT
)
</insert>
그리고 나서 save 후 실제로 board 객체가 데이터베이스에 저장됐는지 확인
-> 확인은 no값으로 진행된다.
// 게시글 등록 기능을 위한 테스트 생성
@Test
@Order(5)
@DisplayName("게시글 등록 테스트")
public void insertBoardTest() {
int result = 0;
Board board = new Board();
Board findBoard = null;
// member 테이블을 참조 하고 있기 때문에 MEMBER내의 pk값에 해당하는 값만 가져올 수 있다.
board.setWriterNo(3);
board.setTitle("mybatis 게시글");
board.setContent("mybatis로 게시글 등록을 해보았습니다.");
// service 객체에서 save 메소드를 사용
// 영향받은 행의 갯수(result) 반환됨
result = service.save(board);
// save 후 실제로 board 객체가 데이터베이스가 저장됐는지 확인
findBoard = service.findBoardByNo(0);
// 정상적으로 insert 후의 검증 코드
assertThat(result).isGreaterThan(0);
assertThat(findBoard).isNotNull();
}
그러나 Board 값에 no값이 없기 때문에 테스트가 실패된다. -> insert 후 pk값을 받아오고 싶은 게 된다. -> 쿼리문 insert문에 keyProperty, useGeneratedKeys, keyColumn을 추가시켜준다.
기억이 잘 안나니 복습차원으로
useGeneratedKeys : 실제로 insert 후 pk 값을 파라미터로 전달되는 객체에 다시 전달(허용)해주겠다는 옵션 - 기본 false
keyProperty : pk값
keyColumn : 키에 해당하는 컬럼명
<insert id="insertBoard"
useGeneratedKeys="true" keyProperty="no" keyColumn="NO">
그리고 no값이 board의 no와 같은지 검증 코드 뒤에 extracting("no").isEqualTo(board.getNo())를 붙여서 테스트 진행 시 확인할 수 있다.
// 게시글 등록 기능을 위한 테스트 생성
@Test
@Order(5)
@DisplayName("게시글 등록 테스트")
public void insertBoardTest() {
int result = 0;
Board board = new Board();
Board findBoard = null;
// member 테이블을 참조 하고 있기 때문에 MEMBER내의 pk값에 해당하는 값만 가져올 수 있다.
board.setWriterNo(3);
board.setTitle("mybatis 게시글");
board.setContent("mybatis로 게시글 등록을 해보았습니다.");
// service 객체에서 save 메소드를 사용
// 영향받은 행의 갯수(result) 반환됨
result = service.save(board);
// save 후 실제로 board 객체가 데이터베이스에 저장됐는지 확인
findBoard = service.findBoardByNo(0);
// 정상적으로 insert 후의 검증 코드
assertThat(result).isGreaterThan(0);
assertThat(findBoard).isNotNull().extracting("no").isEqualTo(board.getNo());
}
- 게시글 수정(update) 테스트
게시글 수정을 위한 테스트 메소드 생성
@Test
@Order(6)
@DisplayName("게시글 수정 테스트")
public void updateBoardTest() {
int result = 0;
// DB 조회 후 no값을 가져온다.
Board board = service.findBoardByNo(158);
board.setTitle("mybatis 게시글 - 수정");
board.setContent("mybatis로 게시글 등록을 해보았습니다. - 수정");
result = service.save(board);
assertThat(result).isGreaterThan(0);
}
여기서 158은 임의로 DB에 있는 no값을 입력해줬기 때문에 사람마다 다르게 뜰 것임.
테스트를 진행하면 false -> ? : BoardService에서 별도로 구현도 되어있지 않고, result가 0으로 초기화 된 것을 그대로 리턴하기 때문
public int save(Board board) {
int result = 0;
SqlSession session = getSession();
// service에서 직접 db에 접근 x -> DAO에게 넘겨줌
if(board.getNo() != 0) {
// UPDATE
result = dao.updateBoard(session, board);
} else {
// INSERT
result = dao.insertBoard(session, board);
}
DAO의 updateBoard를 호출할 수 있도록 해준다. -> updateBoard 에러 -> Dao에서 메소드 생성
public int updateBoard(SqlSession session, Board board) {
return session.update("boardMapper.updateBoard", board);
}
테스트 진행해보면 에러 발생 -> ? : updateBoard 아이디를 가지는 쿼리문이 없어서 -> board-mapper.xml에서 만들어주기
두 가지 버전 (동적 쿼리 적용, 미적용) 으로 작성
1. 일단 기존에 쓰던 쿼리문 방식대로 작성 (JDBC)
<update id="updateBoard">
UPDATE BOARD
SET
TITLE = #{title},
CONTENT = #{content},
ORIGINAL_FILENAME = #{originalFileName},
RENAMED_FILENAME = #{renamedFileName},
MODIFY_DATE = SYSDATE
WHERE
NO = #{no}
</update>
2. 동적쿼리 사용하여 쿼리문 작성 (null이 들어오면 값이 변경되지 않게), 다중 if 구문 사용
<update id="updateBoard">
UPDATE BOARD
SET
<if test="title != null">
TITLE = #{title},
</if>
<if test="content != null">
CONTENT = #{content},
</if>
<if test="originalFileName != null">
ORIGINAL_FILENAME = #{originalFileName},
</if>
<if test="renamedFileName != null">
RENAMED_FILENAME = #{renamedFileName},
</if>
MODIFY_DATE = SYSDATE
WHERE
NO = #{no}
</update>
-> null이 아닌 컬럼은 변경되고, null인 컬럼은 변경되지 않는 것을 확인할 수 있다.
-> 조건들 중 하나만 만족하여 콤마(,)로 끝난다면, 콤마 다음에 WHERE 절이 오므로 문법오류(SyntaxError)가 발생한다. -> 해결 방법 1. set 태그 사용, 2. trim 구문 사용
- set태그 사용하는 방법
<!-- 3. <set>과 다중 <if> 구문을 활용 -->
<update id="updateBoard">
UPDATE BOARD
<set>
<if test="title != null">
TITLE = #{title},
</if>
<if test="content != null">
CONTENT = #{content},
</if>
<if test="originalFileName != null">
ORIGINAL_FILENAME = #{originalFileName},
</if>
<if test="renamedFileName != null">
RENAMED_FILENAME = #{renamedFileName},
</if>
MODIFY_DATE = SYSDATE
</set>
WHERE
NO = #{no}
</update>
- trim 구문 사용하는 방법
prefix : trim 구문에서 앞에 붙여넣어줄 태그를 지정한다.
suffixOverrides : 특정 문자가 오면 지워준다.
<!-- 4. <trim>과 다중 <if> 구문을 활용 -->
<update id="updateBoard">
UPDATE BOARD
<trim prefix="SET" suffixOverrides=",">
<if test="title != null">
TITLE = #{title},
</if>
<if test="content != null">
CONTENT = #{content},
</if>
<if test="originalFileName != null">
ORIGINAL_FILENAME = #{originalFileName},
</if>
<if test="renamedFileName != null">
RENAMED_FILENAME = #{renamedFileName},
</if>
MODIFY_DATE = SYSDATE
</trim>
WHERE
NO = #{no}
</update>
- 게시글 삭제(DELETE) 테스트
앞서 등록, 수정 처럼 똑같이 테스트 코드 생성
@Test
@Order(7)
@DisplayName("게시글 삭제 테스트")
public void deleteTest() {
int result = 0;
result = service.delete(158);
assertThat(result).isPositive().isEqualTo(1);
}
delete에서 에러 발생 -> BoardService에 메소드 생성 -> 로직 만들기~
public int delete(int no) {
int result = 0;
SqlSession session = getSession();
// 실제로는 dao에게 파라미터값을 전달하면서
// 리턴값을 요청하기 때문에 dao에 updateStatus 메소드 만들어서 호출
result = dao.updateStatus(session, no, "N");
// commit, rollback 작업
if (result > 0) {
session.commit();
} else {
session.rollback();
}
session.close();
return result;
}
updateStatus가 없기 때문에 에러 뜸 -> BoardDao에 생성 -> 로직 작성~
public int updateStatus(SqlSession session, int no, String status) {
// 파라미터는 하나만 받을 수 있기 때문에 쿼리에 두 개 이상을 전달해주고 싶을 땐
// map객체를 만들어서 넘겨주면 된다.
Map<String, Object> map = new HashMap<>();
map.put("no", no);
map.put("status", status);
return session.update("boardMapper.updateStatus", map);
}
하고 테스트를 진행하면 updateStatus라는 id를 가지는 쿼리가 없기 때문에 에러발생 -> 쿼리문 만들어주면 된다.(board-mapper.xml)
<update id="updateStatus" parameterType="map">
UPDATE BOARD SET STATUS = #{status} WHERE NO = #{no}
</update>
그리고 테스트 진행하면 통과.
@Test
@Order(6)
@DisplayName("게시글 수정 테스트")
public void updateBoardTest() {
int result = 0;
// DB 조회 후 no값을 가져온다.
Board board = service.findBoardByNo(158);
Board findBoard = null;
board.setTitle("mybatis 게시글 - 수정");
board.setContent("mybatis로 게시글 등록을 해보았습니다. - 수정");
board.setOriginalFileName(null);
board.setRenamedFileName(null);
result = service.save(board);
// 수정이 완료 된 후 새로 조회 시, 실제 title은 업데이트 하기 전 title과 동일할 것이다.
findBoard = service.findBoardByNo(board.getNo());
assertThat(result).isGreaterThan(0);
assertThat(findBoard).isNotNull().extracting("title").isEqualTo(board.getTitle());
}
@Test
@Order(7)
@DisplayName("게시글 삭제 테스트")
public void deleteTest() {
int result = 0;
Board board = null;
result = service.delete(158);
// 수정이 완료 되면 테이블에서 새로 조회해왔을 때,
// where 조건에 STATUS ='Y' 조건 때문에 삭제되어 null로 리턴된다.
board = service.findBoardByNo(158);
assertThat(result).isPositive().isEqualTo(1);
assertThat(result).isNull();
}
삭제되고 나서 새로 조회할 경우 실제 값 조회
@Order6은 새로 조회할 경우 그 실제 값은 업데이트 하기 전 값과 동일할 것이다.
마지막 부분이 살짝 이해가 안되네..흠..
전체 실습 코드
> 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 작업 -->
<!-- column에 'R_'가 생략됐다고 보면 된다. -->
<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>
<!-- 게시글 생성 쿼리문 -->
<!-- 웬만하면 insert되는 컬럼을 명시해주는 것이 좋다.(select도 마찬가지) -->
<insert id="insertBoard" parameterType="Board"
useGeneratedKeys="true" keyProperty="no" keyColumn="NO">
INSERT INTO BOARD (
NO,
WRITER_NO,
TITLE,
CONTENT,
ORIGINAL_FILENAME,
RENAMED_FILENAME,
READCOUNT,
STATUS,
TYPE,
CREATE_DATE,
MODIFY_DATE
) VALUES (
SEQ_BOARD_NO.NEXTVAL,
#{writerNo},
#{title},
#{content},
#{originalFileName},
#{renamedFileName},
DEFAULT,
DEFAULT,
DEFAULT,
DEFAULT,
DEFAULT
)
</insert>
<update id="updateBoard" parameterType="Board">
<!--
1. 기존 JDBC에서 사용하던 쿼리를 활용
UPDATE BOARD
SET
TITLE = #{title},
CONTENT = #{content},
ORIGINAL_FILENAME = #{originalFileName},
RENAMED_FILENAME = #{renamedFileName},
MODIFY_DATE = SYSDATE
WHERE
NO = #{no}
2. 다중 <if> 구문을 활용
UPDATE BOARD
SET
<if test="title != null">
TITLE = #{title},
</if>
<if test="content != null">
CONTENT = #{content},
</if>
<if test="originalFileName != null">
ORIGINAL_FILENAME = #{originalFileName},
</if>
<if test="renamedFileName != null">
RENAMED_FILENAME = #{renamedFileName},
</if>
MODIFY_DATE = SYSDATE
WHERE
NO = #{no}
3. <set>과 다중 <if> 구문을 활용
UPDATE BOARD
<set>
<if test="title != null">
TITLE = #{title},
</if>
<if test="content != null">
CONTENT = #{content},
</if>
<if test="originalFileName != null">
ORIGINAL_FILENAME = #{originalFileName},
</if>
<if test="renamedFileName != null">
RENAMED_FILENAME = #{renamedFileName},
</if>
MODIFY_DATE = SYSDATE
</set>
WHERE
NO = #{no}
4. <trim>과 다중 <if> 구문을 활용
-->
UPDATE BOARD
<trim prefix="SET" suffixOverrides=",">
<if test="title != null">
TITLE = #{title},
</if>
<if test="content != null">
CONTENT = #{content},
</if>
<if test="originalFileName != null">
ORIGINAL_FILENAME = #{originalFileName},
</if>
<if test="renamedFileName != null">
RENAMED_FILENAME = #{renamedFileName},
</if>
MODIFY_DATE = SYSDATE
</trim>
WHERE
NO = #{no}
</update>
<update id="updateStatus" parameterType="map">
UPDATE BOARD SET STATUS = #{status} WHERE NO = #{no}
</update>
</mapper>
> 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.DisplayName;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
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;
@TestMethodOrder(OrderAnnotation.class)
@DisplayName("BoardService 테스트")
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"
)
@Order(1)
@DisplayName("게시글 목록 조회(검색 기능 적용) 테스트")
// 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")
@Order(2)
@DisplayName("게시글 수 조회(필터 적용) 테스트")
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")
@Order(3)
@DisplayName("게시글 목록 조회(필터 적용) 테스트")
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})
@Order(4)
@DisplayName("게시글 상세 조회(댓글 포함) 테스트")
//게시글 상세 데이터 조회를 위한 테스트 메소드 생성
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);
}
// 게시글 등록 기능을 위한 테스트 생성
@Test
@Order(5)
@DisplayName("게시글 등록 테스트")
public void insertBoardTest() {
int result = 0;
Board board = new Board();
Board findBoard = null;
// member 테이블을 참조 하고 있기 때문에 MEMBER내의 pk값에 해당하는 값만 가져올 수 있다.
board.setWriterNo(3);
board.setTitle("mybatis 게시글");
board.setContent("mybatis로 게시글 등록을 해보았습니다.");
// service 객체에서 save 메소드를 사용
// 영향받은 행의 갯수(result) 반환됨
result = service.save(board);
// save 후 실제로 board 객체가 데이터베이스에 저장됐는지 확인
findBoard = service.findBoardByNo(0);
// 정상적으로 insert 후의 검증 코드
assertThat(result).isGreaterThan(0);
assertThat(findBoard).isNotNull().extracting("no").isEqualTo(board.getNo());
}
@Test
@Order(6)
@DisplayName("게시글 수정 테스트")
public void updateBoardTest() {
int result = 0;
// DB 조회 후 no값을 가져온다.
Board board = service.findBoardByNo(158);
Board findBoard = null;
board.setTitle("mybatis 게시글 - 수정");
board.setContent("mybatis로 게시글 등록을 해보았습니다. - 수정");
board.setOriginalFileName(null);
board.setRenamedFileName(null);
result = service.save(board);
// 수정이 완료 된 후 새로 조회 시, 실제 title은 업데이트 하기 전 title과 동일할 것이다.
findBoard = service.findBoardByNo(board.getNo());
assertThat(result).isGreaterThan(0);
assertThat(findBoard).isNotNull().extracting("title").isEqualTo(board.getTitle());
}
@Test
@Order(7)
@DisplayName("게시글 삭제 테스트")
public void deleteTest() {
int result = 0;
Board board = null;
result = service.delete(158);
// 수정이 완료 되면 테이블에서 새로 조회해왔을 때,
// where 조건에 STATUS ='Y' 조건 때문에 삭제되어 null로 리턴된다.
board = service.findBoardByNo(158);
assertThat(result).isPositive().isEqualTo(1);
assertThat(result).isNull();
}
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)
);
}
}
> 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;
}
public int save(Board board) {
int result = 0;
SqlSession session = getSession();
// service에서 직접 db에 접근 x -> DAO에게 넘겨줌
if(board.getNo() != 0) {
// UPDATE
result = dao.updateBoard(session, board);
} else {
// INSERT
result = dao.insertBoard(session, board);
}
// commit, rollback 작업
if (result > 0) {
session.commit();
} else {
session.rollback();
}
session.close();
return result;
}
public int delete(int no) {
int result = 0;
SqlSession session = getSession();
// 실제로는 dao에게 파라미터값을 전달하면서
// 리턴값을 요청하기 때문에 dao에 updateStatus 메소드 만들어서 호출
result = dao.updateStatus(session, no, "N");
// commit, rollback 작업
if (result > 0) {
session.commit();
} else {
session.rollback();
}
session.close();
return result;
}
}
> 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);
}
public int insertBoard(SqlSession session, Board board) {
return session.insert("boardMapper.insertBoard", board);
}
public int updateBoard(SqlSession session, Board board) {
return session.update("boardMapper.updateBoard", board);
}
public int updateStatus(SqlSession session, int no, String status) {
// 파라미터는 하나만 받을 수 있기 때문에 쿼리에 두 개 이상을 전달해주고 싶을 땐
// map객체를 만들어서 넘겨주면 된다.
Map<String, Object> map = new HashMap<>();
map.put("no", no);
map.put("status", status);
return session.update("boardMapper.updateStatus", map);
}
}
'국비지원_Java > Java Programming_2' 카테고리의 다른 글
자바 프로그래밍_Day_101_스프링 개요 (0) | 2022.01.21 |
---|---|
자바 프로그래밍_Day_100_Framework : MyBatis 정리 / log4j (0) | 2022.01.19 |
자바 프로그래밍_Day_98_Framework : MyBatis 동적 쿼리 / 게시글 조회 (0) | 2022.01.12 |
자바 프로그래밍_Day_97_Framework : MyBatis 동적 SQL (0) | 2022.01.08 |
자바 프로그래밍_Day_96_Framework : MyBatis / 동적 쿼리 (0) | 2022.01.06 |