- 제목, 내용, 작성자와 같은 단일 항목
- 제목 + 내용, 제목 + 작성자와 같은 복합 항목
- 검색 항목에 따라서 매번 다른 SQL이 처리될 필요가 있는 상황
- MyBatis의 동적쿼리기능을 이용해서 처리
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
if와 달리 choose는 여러 상활들 중 하나의 상황에서만 동작한다.
<foreach>
태그 내에서 item="val"
과 index="key"
는 각각 key-value 쌍의 '값(value)'와 '키(key)'를 지정합니다. 이 두 변수는 반복되는 블록 내에서 사용되며, 각 반복마다 해당하는 key와 value를 가리킵니다.
예를 들어, map
이 {a: 1, b: 2, c: 3}
이라고 하면:
key
는 'a'
이고, val
은 1
입니다.key
는 'b'
이고, val
은 2
입니다.key
는 'c'
이고, val
은 3
입니다.이렇게 각 반복마다 key
와 val
이 갱신되어 해당하는 키와 값에 접근할 수 있습니다. <if test="key == 'c'.toString()">
와 같은 조건문에서는 이 key
를 사용하여 특정 조건을 검사할 수 있습니다.
item
자체는 MyBatis의 <foreach>
태그에서 사용되는 속성명입니다. 이 속성명(item
)은 변경할 수 없습니다. 그러나 item
속성의 값(여기서는 "val")은 임의로 설정할 수 있습니다. 이 값은 반복되는 각 요소를 대표하는 변수 이름으로 사용됩니다.
예를 들어, 다음과 같이 할 수 있습니다:
<foreach item="element" index="key" collection="map">
<!-- 여기에서 element는 각 원소의 값, key는 각 원소의 키 -->
</foreach>
여기에서 element
는 임의로 지정한 변수명입니다. 이 변수는 <foreach>
블록 내에서 해당 반복의 각 요소값을 가리킵니다. 다시 말해, item
이라는 속성명은 변경할 수 없지만, 그 뒤에 오는 값("val", "element" 등)은 임의로 설정할 수 있습니다.
페이징 처리에 사용했던 Criteria의 의도는 'pageNum'과 'amount'라는 파라미터를 수집하기 위해서였다. 페이징 처리에 검색 조건 처리가 들어가면 Criteria 역시 변화가 필요하다.
pageNum이나 amount 같은 경우는 값을 입력하지 않았을 때, 첫번째 페이지와 10개씩 끊어서 보여주는게 필요하므로 초기화를 한다. 하지만 검색의 경우는 초기에는 검색조건울 넣지 않으므로 초기화 할 필요가 없다.
동적인 SQL을 처리하기 위해 xml을 수정한다. 일단, SQL 구문을 오류가 발생하기 않도록,
indentation을 맞춰준다. inlineview의 괄호같은 경우도 아래와 같이 들여쓰기 한다.
<select id ="getListWithPaging" resultType="com.zerock.domain.BoardVO">
<![CDATA[
select
bno, title, content, writer, regDate, updateDate
from
(
select
rownum rn, bno, title, content, writer, regDate, updateDate
from
tbl_board
where
rownum <= #{pageNum}*#{amount}
)
where
rn > (#{pageNum} -1) * #{amount}
]]>
</select>
위의 쿼리는 복합쿼리이다.
이 때, 검색조건은 안에 있는 Where절에 들어가야 할까? 아니면 밖에 있는 Where절에 들어가야 할까?
inner query, 그러니까 inlineview를 만드는 이유가 뭔지부터 생각해보자.
inlineview에서 rownum을 지정을 하고, 그 rownum을 필터링을 하기 위해서 outer를 이용한다.
즉, 다시 말하면 중요한 기본 쿼리는 내부에 위치한다는 뜻이다.
그래서 안쪽에 있는 where절에 동적쿼리를 작성한다.
CDATA를 분리해서 다음과 작성한다.
<select id ="getListWithPaging" resultType="com.zerock.domain.BoardVO">
<![CDATA[
select
bno, title, content, writer, regDate, updateDate
from
(
select
rownum rn, bno, title, content, writer, regDate, updateDate
from
tbl_board
where
rownum <= #{pageNum}*#{amount}
)
]]> // 추가된 코드
<![CDATA[ // 추가된 코드
where
rn > (#{pageNum} -1) * #{amount}
]]>
</select>
<trim>
을 이용해서 원하는 내용을 접합할 수 있다.
<when test="">
조건이 맞는 것들을 고르라는 의미이다.
<foreach item="type" collection="">
그런데 위 코드를 보자. Criteria에서 type의 타입을 지정할 때 우리는 Collection으로 지정하지 않았다. 문자열로 지정했다. 그런데 foreach 문 안에서는 collection으로 돌아야한다. 그럼 type이라는 건 collection이 아닌데 위의 코드에 어떻게 집어넣을까?
MyBatis의 <foreach>
태그에서 collection 속성은 순회할 컬렉션을 지정한다. 이 속성에 지정된 변수나 표현식은 여러 요소를 포함하는 배열, 리스트, 또는 맵과 같은 컬렉션 데이터 타입이어야 한다. 이 때문에 String타입의 type을 가공해야 할 필요가 있다.
배열로 만들어주는 특정한 함수를 선언한다.
public String[] getTypeArr() {
return type == null ? new String[] {} : type.split("");
}
type
변수가 null
이라면, 빈 배열 new String[] {}
를 반환한다.type
변수에 값이 있다면, split("")
메서드를 사용하여 각 문자를 분리한다. 예를 들어, type
이 "TCW"
라면 ["T", "C", "W"]
와 같은 배열을 반환한다.<trim prefix="(" suffix=") AND" prefixOverrides="OR">
<foreach item="type" collection="typeArr">
<foreach item="type" collection="typeArr">
이라는 구문에서 item="type"
은 typeArr
컬렉션의 각 요소를 순회하면서 그 값을 type
라는 변수에 할당하라는 의미입니다.
예를 들어, typeArr
배열이 ["T", "C", "W"]
로 구성되어 있다면, <foreach>
루프는 다음과 같은 순서로 실행됩니다:
type = "T"
type = "C"
type = "W"
이 type
변수는 <when test="type == 'T'.toString()">
, <when test="type == 'C'.toString()">
, <when test="type == 'W'.toString()">
등의 테스트 조건에서 사용됩니다. 각 순회마다 type
의 값이 바뀌므로, 이에 따라 다르게 처리되는 로직을 작성할 수 있습니다.
여기서 함수의 이름이 getTypeArr인데 이 함수를 getter 함수라 한다. 이 함수는 property가 typeArr인 것과 연동이 된다. 즉, get을 지우고 첫 글자를 소문자로 바꾼 property와 getter함수가 연동이 된다.
Java의 Spring Framework와 MyBatis를 함께 사용할 때, 객체의 getter 메서드는 MyBatis 쿼리에서 해당 객체의 프로퍼티를 참조할 수 있게 해줍니다.
예를 들어, getTypeArr()
라는 getter 메서드가 있는 객체를 MyBatis 쿼리에 전달하면, 이 메서드는 typeArr
라는 이름으로 MyBatis 쿼리 내에서 접근할 수 있게 됩니다.
즉, <foreach item="type" collection="typeArr">
부분에서 collection="typeArr"
이라고 명시했다면, MyBatis는 내부적으로 getTypeArr()
메서드를 호출하여 실제로 순회할 배열을 얻어옵니다. 이 배열은 type.split("")
에 의해 생성되거나, type
이 null
인 경우에는 빈 배열이 됩니다.
이러한 방식으로 getTypeArr()
getter 메서드와 MyBatis의 collection="typeArr"
이 연동됩니다. 이를 통해 MyBatis는 동적 SQL 쿼리를 생성할 때 필요한 데이터를 얻어올 수 있습니다.
item="type"
이 어색하게 느껴지는 이유는 프로그래밍 언어의 대입 연산자 =
와 XML 혹은 HTML 태그의 속성 지정 구문의 형식이 다르기 때문일 수 있습니다.
프로그래밍 언어에서는 대입 연산을 할 때 변수 = 값
의 형태로 쓰지만, XML이나 HTML에서는 속성="값"
의 형태로 작성합니다. 이 둘은 문법적으로 다르며 다른 목적을 가지고 있습니다.
XML에서 item="type"
과 같은 표현은 "속성과 값"의 형태로, 이것은 "이 태그의 item
속성에 type
이라는 값을 설정한다"라는 의미입니다. 반면, 프로그래밍 언어에서 x = 10
은 "변수에 값 대입"을 의미하며, 이는 "변수 x
에 10
이라는 값을 저장한다"라고 해석됩니다.
따라서, 두 문법은 각각의 문맥과 목적에 맞게 디자인되어 있으며, 그래서 처음에는 어색하게 느껴질 수 있습니다. 하지만 각각의 문법과 문맥을 이해하면 자연스럽게 받아들여질 것입니다.
{#keyword}
MyBatis에서는 Java 객체의 속성을 참조할 때 getter 메서드를 통해 접근하는 것이 일반적입니다. 이것은 Java의 캡슐화(encapsulation) 원칙에 따라 객체의 내부 상태를 외부에 직접 노출하지 않고 메서드를 통해 접근하는 것을 권장하기 때문입니다.
예를 들어, Criteria 클래스가 keyword라는 속성과 이에 대응하는 getKeyword()라는 getter 메서드를 가지고 있다면, MyBatis 쿼리에서는 #{keyword}와 같은 표현을 사용하여 해당 값을 가져올 수 있습니다. 내부적으로는 getKeyword() 메서드가 호출되어 그 결과가 사용됩니다.
따라서, XML 쿼리에서 Criteria에 있는 모든 변수에 대해 getter 메서드가 정의되어 있다면, 그 변수들은 #{변수명} 형태로 참조할 수 있을 것입니다. MyBatis는 이를 알아서 처리해 줍니다.
위의 typeArr이 getTypeArr과 연동된다. <foreach>
를 이용해서 검색 조건들을 처리하는데 typeArr이라는 속성을 이용한다. MyBatis는 원하는 속성을 찾을 때 getTypeArr()과 같이 이름에 기반을 두어서 검색하기 떄문에 Criteria에서 만들어둔 getTypeArr() 결과인 문자열의 배열이 <foreach>
의 대상이 된다.
쿼리에서 쓰는 % 표시가 * 와 같다고 생각할수있다.
6||'%' : 6으로 시작하는 항목을 찾는다
||'%'6 : 6으로 끝나는 항목을 찾는다.
'%' ||#{keyword}|| '%': 키워드를 포함하는 모든 항목을 찾는다.
<trim prefix = "OR">
앞에 OR를 붙이고 접합한다 라는 뜻이다.
select rownum rn, bno, title, content, writer, regDate, updateDate from tbl_board
where (title like '%'||1||'%' OR content like '%'||1||'%') AND
rownum <= 100
위를 접합하면 이런식의 코드인것을 확인할수 있다. AND를 쓸때를 () 괄호를 붙여준다.
<trim prefix = "OR">
안쪽의 조건문에서 OR를 붙여야 되는데 맨앞쪽도 붙게된다. 이렇게 되면 구문 오류가 발생하는데 이걸 어떻게 해결갛까
prefixOverrides="OR" 을 추가한다.
위와같이 작성후 테스트를 해본다.
Title에 1이 포함된 모든 데이터를 출력되는 것을 확인할 수 있다.
getTotalCount 부분도 검색을 해서 나온 결과의 수를 확인하기 위해서는 Criteria 객체를 인자로 받아야한다.
<sql>
조각과 <include>
동적 SQL을 이용해서 검색 조건을 처리하는 부분은 해당 데이터의 개수를 처리하는 부분에서도 동일하게 적용되어야 한다. 이 경우 가장 간단한 방법은 동적 SQL을 처리하는 부분을 그대로 복사해서 넣어줄수 있지만, 만일 동적 SQL을 수정하는 경우에는 매번 목록을 가져오는 SQL과 데이터 개수를 처리하는 SQL 쪽을 같이 수정해야 한다.
MyBatis는 <sql>
이라는 태그를 이용해서 SQL의 일부를 별도로 보관하고, 필요한 경우에 include 시키는 형태로 사용할 수 있다.
count에 cri를 전달하도록 코드를 수정한다.
- 페이지 번호가 파라미터로 유지되었던 것처럼 검색 조건과 키워드 역시 항상 화면 이동 시 같이 전송되어야 한다.
- 화면에서 검색버튼을 클릭하면 새로 검색을 한다는 의미이므로 1페이지로 이동한다.
- 한글의 경우 GET방식으로 이동하는 경우 문제가 생길 수 있으므로 주의해야 한다.
list.jsp에 검색 조건과 키워드가 들어갈 수 있게 HTML을 수정한다.
✍list.jsp
기본적인 틀을 위와 같이 작성한다. 브라우저에는 다음과 같이 표시된다.
<form id='searchForm' method="get" action="/board/list">
<select name="type">
<option value="">--</option>
<option value="T">제목</option>
<option value="C">내용</option>
<option value="W">작성자</option>
<option value="TC">제목 + 내용</option>
<option value="TW">제목 + 작성자 </option>
<option value="CW">내용 + 작성자</option>
<option value="TCW">제목 + 내용 + 작성자</option>
</select>
<input type="text" name="keyword"/>
<input type="hidden" name="pageNum" value="${pageMaker.cri.pageNum}">
<input type="hidden" name="amount" value="${pageMaker.cri.amount}">
<button class='btn btn-default'>Search</button>
</form>
form에 id를 지정한 후 스크립트에서 원하는 동작을 작성한다.
$("#searchForm button").on("click", function(e){
//alert($(this));
e.preventDefault();
$("#searchForm").submit();
});
<option>
태그 중 하나에만 selected 속성이 붙을 수 있다. selected 속성은 해당 항목이 화면에서 선택된 항목으로 보이게 한다.
<input type='text' name='keyword' value = "${pageMaker.cri.keyword}" >
위의 코드를 추가해서 아래와 같이 작성한다.
위와 같이 코드를 작성하게 되면,
검색 후에 type과 keyword가 유지가 되는 것을 확인할 수 있다.
$("#searchForm button").on("click", function(e) {
//alert($(this));
e.preventDefault();
if (!$("#searchForm").find("option:selected").val()) {
alert("검색 종류를 선택하세요.");
return;
}
$("#searchForm").submit();
});
return문이 있기 때문에 submit이 진행되지않는다.
위의 코드에서 만약 e.preventDefault를 뒤에 넣으면 어떻게 될까?
$("#searchForm button").on("click", function(e){
//alert($(this));
if (!$("#searchForm").find("option:selected").val()) {
alert("검색 종류를 선택하세요.");
return;
}
e.preventDefault();
$("#searchForm").submit();
});
버튼을 클릭하면 버튼 클릭 이벤트가 발생한다. 그리고 return과 만난 후, 기본(default) 동작인 자동 submit이 발생한다. preventDefalut()의 위치가 어디냐에 따라서 동작이 달라진다.
$("#searchForm button").on("click", function(e){
//alert($(this));
if (!$("#searchForm").find("option:selected").val()) {
alert("검색 종류를 선택하세요.");
return false;
}
e.preventDefault();
$("#searchForm").submit();
});
클릭이벤트가 먼저 일어난다. return false
는 이후의 동작은 수행하지 않는다는 의미이다. 따라서 기본 동작인 자동 submit은 발생하지 않는다. 버튼을 클릭하면 커스터마이즈된 버튼 클릭이 된 이벤트가 발생하고 그 후에 체이닝 동작으로 자동 서밋이 일어나는데 return false는 이 체이닝 된 자동 서밋 동작을 막는다. e.prevenDefault가 존재 하는 이유는 만약 이 동작이 성공을 했을때 기본 제출동작을 막고, $("#searchForm").submit(); 동작을 실행하기 위해서이다.
먼저 기본진행을 막을지 말지 결정
결과적으로 다음과 같이 작성할 수 있다.
$("#searchForm button").on("click", function(e){
//alert($(this));
e.preventDefault();
if (!$("#searchForm").find("option:selected").val()) {
alert("검색 종류를 선택하세요.");
return false;
}
if (!$("#searchForm").find("input[name='keyword']").val()) {
alert("키워드를 입력하세요.");
return false;
}
$("#searchForm").submit();
});
지금 이상태에서 다른 페이지 인덱스를 선택하면
이곳에서 다른곳으로 이동할때 데이터가 전달되지 않는다.
검색 부분 데이터가 전달 되지 않았기 때문이다.
위와 같이 수정한다
이미 modify를 요청을 했을때 Criteria를 인자로 받도록 되어있다.
요청을 하는 내용이 클라이언트에서 시작된다.
조건 검색에 대한 내용을 추가한다.
redirect에 type과 keyword에 대한 내용을 위와 같이 추가한다.
get.jsp에서 제대로 데이터를 넘기는가 확인하고,
컨트롤러에서 인자로 데이터를 잘 받고 뷰로 잘 넘기는가를 다시 한번 확인한다.