2번째 필수 강의 ch 4. 7강 요약
검색어를 입력하고 검색을 누르면
이 때 검색 창은 form으로 받고, 검색 영역은 select option으로 받는다. 컨트롤러는 input으로 들어오는 검색 값만 받을 것이 아니라, 옵션값도 함께 받아야 한다.
동적 쿼리는 <sql>과 <include> 태그를 사용한다. 공통되는 부분은 로 정의하고, <include>로 포함시켜 재사용한다.
여기 공통된 부분을 가진 두 sql 쿼리가 있다.
<select id="select" parameterType="int" resultType="BoardDto">
SELECT bno, title, content, writer, view_count, comment_count, reg_date, up_date
FROM springbasic.board
WHERE bno = #{bno}
</select>
<select id="selectPage" parameterType="map" resultType="BoardDto">
SELECT bno, title, content, writer, view_count, comment_count, reg_Date, up_date
FROM springbasic.board
ORDER BY reg_date desc, bno desc
LIMIT #{offset}, #{pageSize}
</select>
공통된 부분을 <sql>로 뽑아내면,
<sql id="selectFromBoard">
SELECT bno, title, content, writer, view_count, comment_count, reg_Date, up_date
FROM springbasic.board
</sql>
<include>로 재사용할 수 있다.
<select id="select" parameterType="int" resultType="BoardDto">
<include refid="selectFromBoard"/>
WHERE bno = #{bno}
</select>
<select id="selectPage" parameterType="map" resultType="BoardDto">
<include refid="selectFromBoard"/>
ORDER BY reg_date desc, bno desc
LIMIT #{offset}, #{pageSize}
</select>
조건에 따라 특정 sql을 실행하도록 만들려면 <if> 태그를 사용할 수 있다. 여러 조건을 만족할 경우 조건을 만족한 부분의 sql 쿼리문들이 붙는다.
<select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
SELECT count(*)
FROM springbasic.board
WHERE true
<if test="option=='A'">
AND (title LIKE concat('%', #{keyword}, '%')
OR content LIKE concar('%', #{keyword}, '%'))
</if>
<if test="option=='T'">
AND title LIKE concat('%', #{keyword}, '%')
</if>
<if test="option=='W'">
AND writer LIKE concat('%', #{keyword}, '%')
</if>
</select>
여러 조건을 만족할 수 있는 경우가 아니라면, <if>보다는 <choose>가 더 적합할 수 있다. choose는 조건을 만족하는 하나만 선택하여 sql을 추가하므로 훨씬 효율적이다.
<if>가 switch와 비슷하다면, <choose>는 if-else와 비슷하다.
이 상황에서는 검색 기능에서 옵션이 한 번에 하나만 들어오기 때문에 <if>가 아닌 <choose>로 받는 것이 적합하다.
<choose>에서는 만약 조건이 충족되지 않는다면 <otherwise>가 실행된다.
<select id="searchResultCount" parameterType="SearchCondition" resultType="int">
SELECT count(*)
FROM springbasic.board
WHERE true
<choose>
<when test="option=='T">
AND title LIKE concat('%', #{keyword}, '%')
</when>
<when test="option=='W">
AND writer LIKE concat('%', #{keyword}, '%')
</when>
<otherwise>
AND (title LIKE concat('%', #{keyword}, '%')
OR content LIKE concat('%', #{keyword}, '%'))
</otherwise>
</choose>
</select>
이 때 % 는 와일드카드로, 'title%'는 title과 일치하는 값이거나 title 뒤에 n자의 문자가 더 올 수 있음을 의미한다. (0+)
만약 'title_' 이 왔다면, title과 일치하는 값은 허용되지 않고 title 뒤에 n개의 문자가 더 오는 값만 검색된다(1+).
MySQL은 _를 사용하고, Oracle은 ?를 사용한다.
한 번에 여러 결과를 반환하려는데 몇 개의 결과를 받을지가 정해져 있지 않은 경우에는 <foreach>를 사용한다. 조건은 IN으로 받는다.
<select id="getSelected" resultType="BoardDto">
SELECT bno, title, content, writer, view_count, comment_count, reg_Date, up_date
FROM springbasic.board
WHERE bno IN
<foreach collection="array" item="bno" open="(" close=")" separator=",">
#{bno}
</foreach>
ORDER BY reg_date DESC, bno DESC
</select>
IN 뒤에 open과 close로 감싸이고 separator로 구분된 배열이 만들어져 쿼리문이 실행된다.
우선 제목만 가지고 검색하는 기능을 만들어본다. 기본적인 쿼리를 성공시킨 후 동적 쿼리로 변경하기로 한다. 검색할 때 받는 옵션과 키워드는 SearchCondition이라는 객체로 묶어 넘겨주는 것으로 하였다.
먼저 mybatis-config.xml에 alias를 추가해준다.
<typeAlias alias="SearchCondition" type="com.fastcampus.ch4.domain.SearchCondition"/>
다음으로 sql 쿼리를 작성한다.
<select id="searchSelectPage" parameterType="SearchCondition" resultType="BoardDto">
SELECT bno, title, content, writer, view_count, comment_count, reg_Date, up_date
FROM springbasic.board
WHERE true
AND title LIKE concat('%', #{keyword}, '%')
ORDER BY reg_date DESC, bno DESC
LIMIT #{offset}, #{pageSize}
</select>
추가로 검색 결과 개수를 반환하는 쿼리도 추가해준다.
<select id="searchResultCount" parameterType="SearchCondition" resultType="BoardDto">
SELECT count(*)
FROM springbasic.board
WHERE true
AND title LIKE concat('%', #{keyword}, '%')
ORDER BY reg_date DESC, bno DESC
LIMIT #{offset}, #{pageSize}
</select>
다음으로 DAO에 새로운 쿼리를 호출하는 메서드를 추가해주고, BoardDao의 인터페이스에도 이를 추가해준다.
@Override
public int searchResultCount() throws Exception{
return session.selectOne(namespace+"searchResultCount" );
}
@Override
public List<BoardDto> searchSelectPage(SearchCondition sc) throws Exception{
return session.selectList(namespace+"searchSelectPage", sc);
}
domain 패키지에 SearchCondition 클래스를 생성한다.
public class SearchCondition {
private Integer page = 1;
private Integer pageSize = 10;
private Integer offset = 0;
private String keyword = "";
private String option = "";
입력 값이 없을 때를 대비하여 기본값을 설정해준다. getter와 setter, 그리고 생성자 메서드를 함께 생성해준다 (생성자 메서드는 offset을 제외한다).
@Test
public void searchSelectPageTest() throws Exception{
boardDao.deleteAll();
for (int i=1;i<=20;i++){
BoardDto boardDto = new BoardDto("title" + i, "content", "asdf");
boardDao.insert(boardDto);
}
SearchCondition sc = new SearchCondition(1, 10, "title2", "T");
List<BoardDto> list = boardDao.searchSelectPage(sc);
System.out.println("list = " + list);
assertTrue(list.size()==2);
}
제목만 가지고 조회한 결과를 반환한다. 항상 같은 결과를 낼 수 있도록 db에 있는 데이터를 전부 지우고, 테스트를 실행할 때마다 새로 데이터를 넣어서 결과를 확인할 수 있도록 한다.
20개의 데이터를 만들어 집어넣었을 때, title2로 시작하는 제목의 게시글은 title2, title20 2개가 있으므로 assertTrue로 결과를 확인한다.
문제 없이 통과하는 것을 확인할 수 있었다.
searchResultCount()도 테스트하려는데 오류가 났다. 어떤 SQL문이 실행되고 있는지 확인하기 위해
다시 실행해보면 실행되는 sql문들이 로그에 찍힌다. 보니 count를 구하는데 LIMIT와 ORDER BY가 붙어서 sql 오류가 발생하고 있었다. 이 부분을 삭제해주면 NullPointerException가 새롭게 발생한다.
> searchResultCount의 resultType을 int로 고쳐주고, SearchCondition을 받도록 바꿔주면 제대로 결과를 받는 것을 확인할 수 있었다.