1cm

자바 프로그래밍_Day_99_MyBatis 게시글 등록 / 수정 / 삭제 본문

국비지원_Java/Java Programming_2

자바 프로그래밍_Day_99_MyBatis 게시글 등록 / 수정 / 삭제

dev_1cm 2022. 1. 16. 18:22
반응형

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);
	}


}
반응형
Comments