1.  검색 / 필터링 조건의 정의

📍  대부분의 서비스에서는 검색 기능을 제공. 단순히 제목이나 내용 등을 검색하는 경우도 있고, 복잡한 검색 조건을 필터링 filtering 하는 경우도 있음
📍  검색 search는 'A 혹은 B 혹은 C'와 같이 찾고자 하는 경우 검색 조건들은 주로 OR 조건으로 연결되는 경우가 많음
        ➡️  예를 들어 '제목 or 내용 or 작성자'가 xxx인 경우와 같은 데이터가 있을수도 있고, 없을 수도 있음
📍  필터링 filtering'A인 동시에 B에도 해당'한다는 개념. 필터링은 주로 특정한 범위나 범주의 값으로 걸러내는 방식
        ➡️  예를 들어 '완료된 일 중에서 특정 날짜까지 끝난 Todo'는 'A & B'와 같이 AND라는 개념의 필터링이 적용

 

다음과 같은 검색과 필터링 조건을 구성하고 처리

 

  • 제목 title과 작성자 writer 는 키워드 keyword를 이용하는 검색 처리
  • 완료 여부를 필터링 처리
  • 특정한 기간을 지정 from, to 한 필터링 처리
  • 검색과 필터링에 필요한 데이터는 다음과 같이 구분
    * 제목, 작성자 검색에 사용하는 문자열 - keyword
    * 완료 여부에 사용되는 boolean 타입 - finished
    * 특정 기간 검색을 위한 LocalDate 변수 2개 - from, to

 


2.  검색 / 필터링 조건의 결정

검색 기능을 개발할 때는 우선 검색 기능의 경우의 수를 구분하는 작업이 필요

  • 검색 / 필터링의 종류가 '완료 여부, 제목, 작성자, 기간'들의 조합으로 구성
  • 검색 종류를 types라고 지정해서 '제목(t), 작성자(w)'로 구분해서 검색의 실제 값은 검색 종류에 따라 키워드 keyword를 이용
  • 검색은 목록 기능에 사용하는 PageRequestDTO에 필요한 변수들을 추가해서 구성
PageRequestDTO에 types, keyword, finished, from, to 변수를 새롭게 추가
    private String[] types; // 검색 경우의 수 1) title 2) writer 3) title, writer
    private String keyword; // 검색어
    private boolean finished;
    private LocalDate from;
    private LocalDate to;

 


3.  types에 따른 동적 쿼리

Mybatis에는 실행 시에 쿼리를 만들 수 있는 여러 태그들을 제공

 

1) foreach, if

 

🤓  <foreach>는 반복 처리를 위해 제공
🤓  <foreach>의 대상은 배열이나 List, Map, Set과 같은 컬렉션 계열이나 배열을 이용

 

실습을 위해 {"t", "w"}와 같은 types를 PageRequestDTO에 설정하고 테스트를 진행
기존의 TodoMapperTests 클래스에 새로운 메서드를 추가
@Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("AAAA")
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));
    }

 

📍 TodoMapper의 selectList()는 PageRequestDTO를 파라미터로 받고 있기 때문에 변경 없이 바로 사용 가능하므로 TodoMapper.xml만 수정하면 됨

 

TodoMapper.xml에서는 <select id="selectList"> 태그에 MyBatis의 <foreach>를 적용
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
    select * from tbl_todo 
    <foreach collection="types" item="type">
        #{type}
    </foreach>    
    order by tno desc limit #{skip}, #{size}
</select>

 

  👾  현재 PageRequestDTO의 types는 {"t", "w"}이므로 테스트 코드를 실행하면 다음과 같은 코드가 만들어지는 것을 확인
        (쿼리문이 아직 완성된 상태가 아니라서 에러가 발생)

 

  👾  sql 부분만 보면 다음과 같이 sql 이 실행
        select * from tbl_todo ? ? order by tno desc limit ?, ?
        ✓  't'와 'w'가 전달되었기 때문에 'from tbl_todo' 뒤에 두 개의 "?"가 생성된 것을 확인.

 

<if> 적용
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
    select * from tbl_todo 
    <foreach collection="types" item="type" open="(" close=")" separator=" OR">
        <if test="type == 't'.toString()">
            title like concat('%', #{keyword}, '%')
        </if>
        <if test="type =='w'.toString()">
            writer like concat('%', #{keyword}, '%')
        </if>
    </foreach>    
    order by tno desc limit #{skip}, #{size}
</select>

 

  👾  open과 close를 이용해서 '()'와 배열을 처리하면서 중간에는 OR을 추가해서 다음과 같은 쿼리가 생성
       select * from tbl_todo (title like concat('%', ?, '%') OR

           writer like concat('%', ?, '%'))

       order by tno desc limit ?, ?


2)  <where>


📍 쿼리에 where 키워드가 빠져있음. 이것은 만일에 types가 없는 경우에는 쿼리문에 where를 생성하지 않기 위함.
📍 <where>는 태그 안쪽에서 문자열이 생성되어야만 where 키워드를 추가.
📍 이를 이용해서 types가 null이 아닌 경우를 같이 적용하면 다음과 같이 작성.

<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
    select * from tbl_todo 
    <where>
        <if test="types != null and types.length > 0">
            <foreach collection="types" item="type" open="(" close=")" separator=" OR">
                <if test="type == 't'.toString()">
                    title like concat('%', #{keyword}, '%')
                </if>
                <if test="type =='w'.toString()">
                    writer like concat('%', #{keyword}, '%')
                </if>
            </foreach>
        </if>
    </where>
    order by tno desc limit #{skip}, #{size}
</select>

 


3) <trim>과 완료 여부 / 만료일 필터링


완료 여부 (f)와 만료 기간 (d)에 대한 처리
  📍  완료 여부는 PageRequestDTO의 finished 변수 값이 true인 경우에만 'finished = 1'과 같은 문자열이 만들어지도록 구성
         ✓  주의점은 앞의 검색 조건(제목, 작성자)이 있는 경우에는 'and finished = 1'의 형태로 만들어져야 하고,
              그렇지 않은 경우에는 바로 'finished = 1'이 되어야 함
  📍  MyBatis의 <trim>이 이런 경우에 사용. <where>과 유사하게 동작하면서 필요한 문자열을 생성하거나 제거할 수 있음

        <if test = "finished">
            <trim prefix="and">
                finished = 1
            </trim>
        </if>
    </where>    
    order by tno desc limit #{skip}, #{size}
</select>

 

  👾  finished 값을 이용해서 SQL 문을 생성해 내는데 <trim>을 적용. prefix를 적용하게 되면 상황에 따라서 'and'가 추가.

 

테스트 코드 수정해서 확인
@Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("스프링")
                .finished(true)
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));
    }

 

같은 방식으로 만료일 dueDate 을 처리하면 다음과 같음
<if test="from != null and to != null">
    <trim prefix="and">
        dueDate between #{from} and #{to}
    </trim>
</if>

 


4) <sql> 과 <include>


MyBatis의 동적 쿼리 적용은 단순히 목록 데이터를 가져오는 부분과 전체 개수를 가져오는 부분에도 적용되어야 함
  📍전체 개수를 가져오는 TodoMapper의 getCount()에 파라미터에 PageRequestDTO 타입을 지정한 이유 역시 동적 쿼리를 적용하기 위함
  📍 MyBatis에는 <sql> 태그를 이용해서 동일한 SQL 조각을 재사용할 수 있는 방법을 제공
        ➡️  동적 쿼리 부분을 <sql>로 분리하고 동일하게 동적 쿼리가 적용될 부분은 <include>를 이용해서 작성

 

TodoMapper.xml에서 <sql id='search'>로 동적 쿼리 부분을 분리
<sql id="search">
    <where>
        <if test="types != null and types.length > 0">
            <foreach collection="types" item="type" open="(" close=")" separator=" OR">
                <if test="type == 't'.toString()">
                    title like concat('%', #{keyword}, '%')
                </if>
                <if test="type =='w'.toString()">
                    writer like concat('%', #{keyword}, '%')
                </if>
            </foreach>
        </if>
        <if test = "finished">
            <trim prefix="and">
                finished = 1
            </trim>
        </if>
        <if test="from != null and to != null">
            <trim prefix="and">
                dueDate between #{from} and #{to}
            </trim>
        </if>
    </where>
</sql>
    <select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
        select * from tbl_todo 
        <include refid="search"></include>
        order by tno desc limit #{skip}, #{size}
    </select>

    <select id="getCount" resultType="int">
        select COUNT(tno) FROM tbl_todo
        <include refid="search"></include>
    </select>

 

테스트 코드에서는 TodoMapper의 selectList()와 getCount()를 호출해서 결과를 확인
    @Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("스프링")
                //.finished(true)
                .from(LocalDate.parse("2022-04-28"))
                .to(LocalDate.parse("2024-04-30"))
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));

        log.info(todoMapper.getCount(pageRequestDTO));
    }

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts