1cm

자바 프로그래밍_Day_97_Framework : MyBatis 동적 SQL 본문

국비지원_Java/Java Programming_2

자바 프로그래밍_Day_97_Framework : MyBatis 동적 SQL

dev_1cm 2022. 1. 8. 00:43
반응형

 

2022. 01. 04

페이징 처리, 동적 SQL 실습

 

동적 SQL 실습

 

 > board-mapper.xml

	<resultMap type="com.kh.mybatis.member.model.vo.Member" 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="createDate" column="CREATE_DATE"/>
		<result property="modifyDate" column="MODIFY_DATE"/>
	</resultMap>

mapper 내의 resultMap의 type에 풀 패키지 명을 적어주는 것은 비효율 적이므로 mybatis-config.xml -> <typeAliases>내에서 별칭을 부여해준다.

 

	<!-- 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"/>
	</typeAliases>

별칭을 부여해준 뒤 다시 board-mapper.xml로 돌아와서 resultMap의 type을 별칭으로 적어주면 자동으로 적용이 된다.

 

<mapper namespace="boardMapper">
	<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="createDate" column="CREATE_DATE"/>
		<result property="modifyDate" column="MODIFY_DATE"/>
	</resultMap>

 

resultMap 내부 작성

id : 테이블에 저장, 조회된 pk값을 객체 특정 필드에 매핑해줄 때 사용.

id-property : Board 타입으로 만들어지는 객체의 필드명 (Board.java에서 가져와서 적어줌 - 아래 이미지)

id-column : 조회된 결과에서 pk에 해당하는 컬럼명

 

 

그리고 아래에 select - resultType에 resultMap의 id값을 적어주어 매핑이 되게 한다.

<select id="selectAll" resultType="boardListResultMap">
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'
</select>

 

 

 > BoardServiceTest.java

	@Test
	// Board 객체를 요소로 담고있는 list를 반환해주는 함수
	public void findAllTest() {
		List<Board> list = null;
		
		// 각각 파라미터를 받는 것 외에는 Null 처리를 한다고 가정
		// title을 받을 때 writer, content는 null로 출력
		String writer = null;
		String title = null;
		String content = "테스트";
		
		list = service.findAll(writer, title, content);
		
		System.out.println(list);
		
		assertThat(list).isNotNull();
	}

 

기존에 getParameter 파라미터 값을 불러왔다면,

밑의 코드처럼 (list = service.findAll(writer, title, content);로 값을 불러오게 함 -> 파라미터 타입도 알아서 넘어가긴 한다.

 

package com.kh.mybatis.board.model.service;

import java.util.ArrayList;
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.SqlSessionTemplate;

public class BoardService {
	// Dao 객체에게 데이터 조회해달라고 요청
	private BoardDao dao = new BoardDao();
	
	public List<Board> findAll(String writer, String title, String content) {
		// Board객체를 가지는 list를 만들겠다- 선언
		List<Board> list = null;
		SqlSession session = SqlSessionTemplate.getSession();
		
		// dao에게서 리스트를 받을 것임
		list = dao.findAll(session, writer, title, content);
		
		session.close();
		
		return list;
	}

}

 

그리고 BoardService.java에 list = dao.findAll(session) 뒤에 writer, title, content를 추가해준 뒤,

에러 삭제를 위해 BoardDao.java - method에 파라미터 writer, title, content를 추가시켜줌

 

public class BoardDao {

public List<Board> findAll(SqlSession session, String writer, String title, String content) {
	
		return session.selectList("boardMapper.selectAll");
	}

}

 

board-mapper.xml select구문에 AND M.ID LIKE = '%' || #{writer} || '%' 추가시켜준다.

위의 방법은 한 개의 값을 넘겨주는 방법인데,

만약 여러 개의 값을 한번에 넘겨주고 싶을 경우 collection객체 중 map에 여러 값을 담아서 넘겨줄 수 있다.

 

 

BoardDao.java에 map객체 생성

public class BoardDao {

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

}

 

 

board-mapper.xml select구문에 if 구문을 추가시켜준다.

WHERE 구문 밑에 아래 if 구문들을 추가시켜준다.

각각의 값들이 Null이 아니면 각각의 쿼리문들이 추가된다.

		<!-- 1. 다중 <if>를 활용한 검색 기능 구현 -->
        
       		<!-- 검색하는 내용만 포함되면 조회가능 -->
        	<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>

 

 

board-mapper.xml -> select 구문에 parameterType을 추가시켜준다

-> 알아서 처리해주긴 하지만 오류가 날 수도 있을 확률이 크기 때문에 적어주는 것이 좋다.

<select id="selectAll" parameterType="map" resultType="boardListResultMap">

 

 

- <where>와 다중 <if>를 활용한 검색 기능 구현해보기

select문 안에 쿼리문 + <where> + 다중<if>구문

<select id="selectAll" parameterType="map" resultType="boardListResultMap">
		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>
	</select>

 

첫 번째 조건 만족 시 해당 쿼리문 실행,

두 번째 조건 부터 만족 시 where 구문의 속성 덕분에 AND / OR 이 삭제 된다.

다 만족하지 않을 경우 AND B.STATUS='Y'가 바로 온다.

 

	@Test
	// Board 객체를 요소로 담고있는 list를 반환해주는 함수
	public void findAllTest() {
		List<Board> list = null;
		
		// 각각 파라미터를 받는 것 외에는 Null 처리를 한다고 가정
		// title을 받을 때 writer, content는 null로 출력
        	// 전부 null일 경우 다중<if>구문 에서 그다음에 있는 B.STATUS='Y'로 넘어가서 전체 데이터가 조회된다.
		String writer = null;
		String title = null;
		String content = null;
		
		list = service.findAll(writer, title, content);
		
		System.out.println(list);
		
		assertThat(list).isNotNull();
	}

String content = null; 부분은 다시 String content = "테스트"; 로 변경해줬음

writer=""; 일 경우 전체 게시물 조회됨, 값이 만약 "xx"이런식으로 있을 경우 그 값만 들어있는 걸 조회해줌

 

 

<trim>과 다중<if>를 활용한 검색 기능 구현

 

		 <!-- 
		 	3.<trim>과 다중<if>를 활용한 검색 기능 구현
		  -->
		  <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>

 

 

각각의 쿼리문 중 만족하는 쿼리문의 시작 부분에 WHERE이 붙고 AND나 OR로 시작을 하는 경우 AND, OR을 삭제 후 쿼리문을 실행 시켜 주고, 만약 모든 쿼리문이 만족하지 않으면 WHERE 뒤에 AND B.STATUS='Y'가 오게 되는데, 이 때 AND는 자동으로 삭제된다.

 

 

<choose>, <when>, <otherwise>를 활용한 검색 기능 구현

만약 넘어가는 값이 writer, title, content 세 가지 다 들어오는 경우 다중 if문을 사용해주는 것이 효율적이다.

 

 

<choose> 구문 안에 <when> 구문을 넣어주면 되는데 여러 개가 와도 상관 없다.

첫 번째 when절 만족 시 1번 when절만 수행되고 나머지는 수행되지 않음, 그러나 첫 번째 when 절 만족하지 않을 시 그 다음 when절로 넘어가게 되고 나머지 when절은 수행되지 않는다.

고로 결론은 만족하는 when 구문 제외하고는 수행이 되지 않는다는.

만약 모든 when 절이 만족하지 않는 경우 포함되어야 할 쿼리가 있다면 <when>구문 뒤에 <otherwise> 구문을 추가시켜준다.

 

 

 

		  <!-- 
		  	4.<choose>, <when>, <otherwise>를 활용한 검색 기능 구현
		   -->
		   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'
		   <choose>
		   		<when test="writer != null">
		   			<!-- wrtier가 있으면 넘겨주는 값으로 해당하는 데이터만 조회되게 해준다. -->
		   			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>

 

choose 구문은 <when> 내에 있는 조건식이 하나이면, 다음 <when>구문은 탐색하지 않고 하나의 만족하는 쿼리문만 실행

다중 if문은 여러 개의 조건을 만족하게 하고 싶다! 싶을 때 사용. -> 각 <if>태그가 가지고 있는 쿼리문 여러 개 실행 가능

 

 

 

 

 

CsvSource 내의 null을 위와같이 입력해주면 문자열 그대로 인식하게 된다.

 

 

value와 nullValues를 활용해서 null값으로 들어갈 수 있게 해준다.

 

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

 

 

필터 관련

 

- SQL DEVELOPER에서 아래 쿼리문 WEB 내에서 실행시켜줌

 

 

BOARD 테이블에 들어간 것을 확인할 수 있다.

 

 

ex.

b1 : 식당에 대한 게시글

b2 : 숙박업체에 대한 게시글

b3 : 관광지에 대한 게시글

임의로 구분지어줌

체크박스 경우 name은 동일, value는 다르다.

	// {"B1", "B3"}
	// {"B1"}
	String[] btype = request.getParameterValues("btype");

위 방식처럼 데이터를 받아서 조회하게끔 가정한다.

 

 

> BoardServiceTest.java

	@Test
	public void findAllTest() {
		// 배열로 데이터 가져오기
		// filters에는 B1, B2 값을 가지고 있다고 가정할 수 있음
		String[] filters = new String[] {"B1", "B2"};	// request.getParameterValues("filter");
		List<Board> list = null;
		
		// 리스트 얻어오기 (from. service 객체)
		list = service.findAll(filters);
		
		assertThat(list).isNotNull();
	}

 

 

 

findAll : ctrl + 1 -> dao 생성

 

해당하는 쿼리문이 없어서 테스트 진행 시 에러 발생함

-> board-mapper.xml에서 selectBoardListByFilters라는 id를 가지고 있는 select문 만들어주기

 

기존 쿼리문에 B.TYPE, AND B.TYPE IN('B2', 'B3'); 구문 추가시켜줌

	<select id="selectBoardListByFilters">
		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'
		  AND B.TYPE IN ('B2', 'B3')
	</select>

 

 

매핑을 위해 Board.java에 type을 추가시켜준다. -> type 값을 조회했을 때 객체 매핑을 위함

그리고 조회된 결과를 resultMap의 매핑 정보를 보고 매핑해줄 수 있도록 id값을 적어준다.

그리고 type에 대한 매핑은 Board.java에서 자동으로 해주지만, result에도 명시적으로 추가를 시켜준다(유지보수 용이)

 

 

if test = "filters != null"을 array로 바꿔준다

-> list, 배열 타입 같은 경우 파라미터로 전달하면 내부적으로는 map 객체로 변환이 되어 넘어간다.

-> key : list / value : object

-> key : array / value : filter

-> 내부적으로 key값은 변수명이 되고, value는 해당하는 오브젝트로 map타입으로 저장이 된다.

만약 filters로 쓰고 싶다면? -> 내부적으로 map타입으로 변환이 되지 않게 BoardDao에서 Map객체 생성 후 filters라는 이름으로 넘겨준다.

 

 

BoardDao.java

	public List<Board> findAll(SqlSession session, String[] filters) {
		/*
		 * List 타입이나 Array 타입의 데이터를 파라미터로 전달하면 내부적으로는 Map으로 타입이 변환되어서 저장되기 때문에
		 * Mapper에서는 list나 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);
		
		return session.selectList("boardMapper.selectBoardListByFilters", map);
	}

 

 

그리고 나서 foreach 구문 추가시켜준다. 

	<select id="selectBoardListByFilters"  resultMap="boardListResultMap">
		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'
		<!-- filters가 null인지 확인 후 null이 아니면If내의 구문 실행 -->
		<if test="filters != null">
		  AND B.TYPE IN 
		  <foreach collection="filters" item="filter" open="(" separator="," close=")">
		  	#{filter}
		  </foreach>
		</if>
	</select>

여러 개의 필터를 선택했을 때, 필터를 만족하는 애들만 조회할 수 있게끔 하는 쿼리문 작성

collection : 배열

item : 배열의 요소들 (filter 말고 다른 이름으로 적어도 됨)

현재는 값이 두개 있기 때문에 두번 반복, 반복할 때마다 필터의 값이 item에 들어가서 출력을 해줄 것임

 

 

 

- 특정 게시글 검색

 

board-mapper.xml에 <sql>로 중복되는 쿼리 제거해주기

 

<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>
...

 

 

그 다음 <include refid="boardListSql" /> 을 기존 쿼리문 대신에 넣어준다.

 

 

중복되는 쿼리문이 사라져서 코드가 훨씬 짧아짐!

단점은 유지보수 시 가독성이 좀 떨어질 수도 있다.

 

 

 

foreach 태그의 활용 정리

 

	<select id="selectBoardListByFilters"  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>

 

 

페이징 처리해주기

 

 

클래스 생성 후 이전에 pageInfo 사용했던 거 복붙해준다.

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

 

 

그 다음 패키지 생성

SqlSessionTemplate 패키지 명에 common.뒤에 template를 붙여주면 에러가 뜨는데

 

ctrl+1 눌러서 Move ~~ 선택해준다.

 

common밑에 template 폴더가 만들어지면서 SqlSessionTemplate가 이동한 것을 확인할 수 있다.

그 다음 MemberService, BoardService에서 에러들이 발생하게 되는데

맥 사용자 기준 command + shift + o(알파벳) 을 입력해주면 import문을 다시 만들어준다. (BoardService 기준)

MemberService에서는 수기로 수정해줬다

변경 전
변경 후

 

 

AppTest.java에서도 import 수정 처리 해주고 테스트 진행

 

게시글 갯수 조회

 

	@Test
	public void getBoardCountTest() {
		int count = 0;
		String[] filters = new String[] {"B2", "B3"};
		
		// 현재 게시글의 갯수 가져오기
		count = service.getBoardCount(filters);
		
		
		// 게시글의 숫자는 양수이면서, 0보다 크다는 것이라고 가정
		assertThat(count).isPositive().isGreaterThan(0);
	}

 

getBoardCount에서 발생하는 에러 ctrl+1눌러서 해결

 

 

BoardService.java에서 getBoardCount 메소드 생성 후 임시로 코드 작성

import 구문 수정

 

기존 SqlSessionTemplate.getSession(); 으로 되어 있던 것 오른쪽처럼 수정

 

 

	public int getBoardCount(String[] filters) {
		// count라는 값을 반환
		int count = 0;
		SqlSession session = getSession();
		
		// dao에서 게시글 갯수 구해오기
		count = dao.getBoardCount(session, filters);
		
		// 조회되는 값이 있으면 session close
		session.close();
		
		return count;
	}

 

 

 

BoardService.java -> getBoardCount에서 게시글 갯수 구해와서 조회할 수 있게 코드 작성 후, 에러 발생하는 부분 추가 메소드 생성

 

테스트를 돌리면 Dao에서 0을 리턴하고 있어서 return 1로 변경 후 테스트 진행하면 통과하는 것을 확인할 수 있음

리턴 값 변경 해주기

String[] 대신 List<String>으로 변경해주면 BoardService.java쪽에서 에러가 발생하게 되는데,

 

 이 경우 BoardService 메소드로 넘어와서 filters를 list로 변경해주면, list로 나오는 결과값을 Dao에게 전달해준다.

테스트 진행 시 에러 발생하게 되는데, 쿼리문이 없어서 그런것 -> 쿼리문 생성 (board-mapper.xml)

board-mapper.xml로 넘어와서 selectBoardListByFilters의 파라미터 타입을 Map으로 설정 후 새로운 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>

 

 

 

전체 실습 코드

 

> 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>
	
	<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>
		-->
		 <!-- 

		 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)
			2. <where>와 다중 <if>를 활용한 검색 기능 구현
		<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>를 활용한 검색 기능 구현
		  <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">
		   			<!-- wrtier가 있으면 넘겨주는 값으로 해당하는 데이터만 조회되게 해준다. -->
		   			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>
</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 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.CsvSource;

import com.kh.mybatis.board.model.vo.Board;

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

	@Test
	public void getBoardCountTest() {
		int count = 0;
		String[] filters = new String[] {"B2", "B3"};
		
		// 현재 게시글의 갯수 가져오기
		count = service.getBoardCount(filters);
		
		
		// 게시글의 숫자는 양수이면서, 0보다 크다는 것이라고 가정
		assertThat(count).isPositive().isGreaterThan(0);
	}
	
	@Test
	public void findAllTest() {
		// 배열로 데이터 가져오기
		// 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();
	}

}

 

 

> 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.SqlSession;

import com.kh.mybatis.board.model.vo.Board;

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) {
		/*
		 * 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);
		
		return session.selectList("boardMapper.selectBoardListByFilters", map);
	}

}

 

 

> 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 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) {
		List<Board> list = null;
		SqlSession session = getSession();
		
		// dao에게 findAll 요청 하면서 매개값 session, filter 넘겨줌
		list = dao.findAll(session, filters);
		
		
		return list;
	}

}

 

 

> SqlSessionTemplate.java

package com.kh.mybatis.common.template;

import java.io.IOException;
import java.io.InputStream;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class SqlSessionTemplate {
	public static SqlSession getSession() {
		InputStream is = null;
		SqlSessionFactoryBuilder builder = null;
		SqlSessionFactory factory = null;
		SqlSession session = null;
		
		try {
			// Resources는 Mybatis에서 제공하는 유틸 클래스로
			// 클래스 패스로부터 자원(Resource)를 쉽게 읽어오는 메소드를 제공한다.
			is = Resources.getResourceAsStream("mybatis-config.xml");
			
			builder = new SqlSessionFactoryBuilder();
			factory = builder.build(is);
//			factory = builder.build(is, "kh");
			
			// true : 오토커밋 활성, false : 오토커밋 비활성
			session = factory.openSession(false);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return session;
	}
}

 

 

 > Board.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 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;
	
}

 

 

 > PageInfo.java

 

package com.kh.mybatis.common.util;

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;
	}
}
반응형
Comments