화면에서 태그를 선택하면, 해당 태그를 가지고 있는 강의를 조회한다.
하나의 강의는 여러 개의 태그를 가질 수 있다.
또 하나의 태그에 대해서 이를 가진 강의가 여러 개 있을 수 있다. (M:N)
검색조건에 따라 강의 정보를 조회하는 쿼리를 작성할 때
원래는 아래처럼 여러 개의 태그를 선택했을 때, 그 중 하나라도 해당이 되는 강의는 모두 조회하도록 했다.
(실제 코딩할 때 사용한 다른 검색조건들은 편의상 생략하고 기록한다.)
<select id="getCoursesByCriteria" parameterType="kr.co.hta.criteria.CourseCriteria" resultMap="CourseResultMap">
select *
from online_courses
<where>
<if test="tags != null">
course_no in (select course_no
from online_course_tags
where course_tag in
<foreach collection="tags" item="tag" open="(" close=")" separator=",">
#{tag}
</foreach>)
</if>
</select>
하지만 여러 개의 태그를 선택했을 때, 그 태그들을 모두 가지는 강의만 조회하도록 하려면 어떻게 해야 할까?
아래와 같은 쿼리를 SQL developer에서 실행해보면,
'java'와 '객체지향' 태그를 모두 가진 강의번호만 조회할 수 있다.
이 때 having절의 2는 조건으로 삼는 태그의 수이다.
online_course_tags
는 course_no, course_tag로 이루어진 테이블이기 때문에
태그 중 'java', '객체지향' 2개를 모두 가지는 특정 강의번호는 2행 이상 조회될 수밖에 없다.
마찬가지로 어떤 태그 조건 3개로 검색했다면 특정 강의번호는 최소 3행 이상 조회되어야 할 것이다.
select course_no
from (select course_no
from online_course_tags
where course_tag in ('java', '객체지향'))
group by course_no
having count(course_no) >= 2;
따라서 having절에 파라미터인 태그 collection의 size를 넣을 수만 있으면 mybatis에서도 이를 표현할 수 있다.
<select id="getCoursesByCourseCriteria" parameterType="kr.co.hta.criteria.CourseCriteria" resultMap="courseResultMap">
select *
from online_courses
<where>
<if test="tags != null">
course_no in (select course_no
from (select course_no
from online_course_tags
where course_tag in
<foreach collection="tags" item="tag" open="(" close=")" separator="," >
#{tag}
</foreach>)
group by course_no
having count(course_no) >= ${tags.size})
</if>
</select>
파라미터가 CourseCriteria
객체이고, 이 객체는 필드로 List<String> tags
를 가지고 있다.
따라서 ${tags.size}
와 같이 작성해서 쿼리의 일부를 완성시키면 된다.
처음에는 #{tags.size}
로 작성했다가 java.lang.UnsupportedOperationException: null 오류가 발생했다.
헷갈렸던 두 가지의 차이를 다시 정리해보았다.
#{}
는 preparedStatement 방식으로 파라미터값을 바인딩해준다. 문자열에 자동으로 따옴표가 붙는다.
${}
는 sql문의 일부로, 값이 그대로 들어가는 것이다. 문자열에 따옴표가 붙지 않는다. => order by 절의 컬럼명을 동적으로 지정하는 등에 활용할 수 있다.
하지만${}
는 악의적인 쿼리 주입을 예방할 수 없어서, 사용자 입력값을 전달할 때는 사용하지 않는 게 좋다.
지금 상황에는 having절에서 ?으로 값을 바인딩할 수 있는 것이 아닌가? 의문이 들었다.
그리고 이 경우에는 넣고자 하는 값이 문자열이 아니라 숫자이기 때문에 #{}를 쓴다고 해서 자동 따옴표가 붙어 문제가 생기는 건 아니었다.
#과 $의 파싱 방식 차이는 이유가 아니고, #{}로는 .size로 콜렉션의 size를 구할 수 없기 때문이었다.
예외명에서 말하듯이 말 그대로 UnsupportedOperation
이라는 것.
https://stackoverflow.com/questions/58654012/collection-size-in-mybatis-query
이 글에서의 답변을 나는 아래처럼 이해했다.
tags.size
는 콜렉션 객체에서 size() 메소드를 호출시키도록 해주는 OGNL(Object-Graph Navigation Language) 표현식으로 작성한 것이다.
#{}
는 mybatis 내부 표현식의 값과 필드를 getter/setter/기본생성자 메소드를 통해 접근하는 것이지 size() 메소드를 지원하지는 않는다.
그런데 그렇다면, 원하는 결과는 얻을 수 있지만 ${}의 사용으로 인해 보안은 굉장히 취약한 쿼리라는 점에서 고민이 필요하기도 한 것 같다.