[MyBatis] 05. 기타
- -
이번 포스트에서는 MyBatis를 쓰면서 알아두면 유용한 내용이나 주의 사항들을 정리해본다.
쿼리 작성 시 주의점
부등호 사용 시 주의점
MyBatis는 일반적으로 XML 문서에 쿼리를 작성한다. XML은 문서의 특성상 <와 > 를 예약어로 사용한다. 따라서 SQL에서 값의 대/소 비교를 위해 부등호를 사용하면 문서가 well-formed 하지 않다는 오류가 발생한다.
The content of elements must consist of well-formed character data or markup.
이 문제를 처리하기 위해 XML의 CDATA (character data) section 안에 쿼리를 작성하면 XML 파서가 아예 파싱하지 않고 단순 문자열로 처리된다.
<select id="selectByGNP" resultType="Country" parameterType="map">
select * from country
<!-- where GNP < #{gnp} -->
<![CDATA[
where GNP < #{gnp}
]]>
</select>
like 처리
MyBatis에서 기본으로 사용하는 PreparedStatement는 ? 를 파라미터로 통으로 대체한다.
이에 따라 만약 like 절을 쓴다면 아래와 같은 구성된다.
select * from country where Name like ?
즉 %나 _ 같은 와이드카드가 개입될 여지가 없다.
따라서 애초에 쿼리를 호출할 때 파라미터에 와이드카드를 포함해서 호출하거나 concat 함수를 이용해서 문자열 결합을 이용해야 한다.
<select id="selectLikeName" resultMap="countryBase" parameterType="map">
select * from country
where Name like concat('%',#{name},'%')
</select>
auto_increment와 <selectKey>
게시판을 구현하기 위해서 테이블을 디자인할 때 많은 경우 P.K를 auto increment로 채번해서 사용한다. 그런데 간혹 방금 삽입된 글의 번호가 필요한 경우가 발생한다. 이처럼 자동 생성된 채번으로 insert 된 경우 채번된 정보를 알고 싶을 경우가 많다.
이런 경우 insert 태그가 가진 useGeneratedKeys 속성과 keyProperty 속성을 사용한다.
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
insert into board (writer, title, content) values (#{writer},#{title},#{content})
</insert>
만약 auto increment 하지 않아서 채번해야 할때는 <selectKey>를 사용할 수 있다.
- order: 데이터가 추가되기 전/후의 시점으로 BEFORE/AFTER 지정 가능(대소문자 가림)
- resultType: 조회 결과의 타입
- keyProperty: 조회된 결과를 저장할 DTO의 속성 명
속성을 보면 알겠지만 특별한 마법이 있는 것은 아니고 데이터가 추가되기 전/후에 키로 사용되는 컬럼의 값을 조회해서 DTO에 자동으로 설정해준다.
다음은 insert가 진행되기 전(BEFORE) 마지막 id 값을 조회해서 DTO의 id에 할당하는 코드이다.
<insert id="insert">
<selectKey order="BEFORE" resultType="int" keyProperty="id">
select max(id) from city
</selectKey>
insert into city (name, countrycode, district, population)
values (#{name}, #{countryCode}, #{district}, #{population})
</insert>
역시 mapper interface를 작성한 후
int insert(City city);
단위테스트를 실행해보면 추가한 City 객체의 id 값이 설정되어있는 것을 알 수 있다.
@Test
@Transactional
public void insertTest() {
// 분명 현재는 id가 null 인상태
City city = new City("newCity", "KOR", "newDistrict", 1000);
int result = cRepo.insert(city);
assertEquals(result, 1);
log.trace("방금 넣은 데이터: {}", city);
}
출력된 로그를 살펴보면 insert 후 select가 진행되고 있으며 city객체를 살펴보면 id가 선명히 남아있는 것을 알 수 있다.
15:20:18 [DEBUG] [c.e.d.m.r.C.insert.debug-137] > ==> Preparing: insert into city (name, countrycode, district, population) values (?, ?, ?, ?)
15:20:18 [DEBUG] [c.e.d.m.r.C.insert.debug-137] > ==> Parameters: newCity(String), KOR(String), newDistrict(String), 1000(Integer)
15:20:18 [DEBUG] [c.e.d.m.r.C.insert.debug-137] > <== Updates: 1
15:20:18 [DEBUG] [c.e.d.m.r.C.insert!selectKey.debug-137] > ==> Preparing: select max(id) from city
15:20:18 [DEBUG] [c.e.d.m.r.C.insert!selectKey.debug-137] > ==> Parameters:
15:20:18 [TRACE] [c.e.d.m.r.C.insert!selectKey.trace-143] > <== Columns: max(id)
15:20:18 [TRACE] [c.e.d.m.r.C.insert!selectKey.trace-143] > <== Row: 4080
15:20:18 [DEBUG] [c.e.d.m.r.C.insert!selectKey.debug-137] > <== Total: 1
15:20:18 [TRACE] [c.e.d.CityRepoTest.insertTest- 43] > 방금 넣은 데이터: City(id=4080, name=newCity, countryCode=KOR, district=newDistrict, population=1000, country=null)
sql 문장의 재사용
mapper에서 자주 사용하는 문장이 있다면 이를 블럭화 해서 재사용할 수 있다.
예를 들어 다음의 태그들을 살펴보자.
<select id="select" resultMap="cityBase" parameterType="int">
select * from city where id=#{id}
</select>
<select id="selectByCountry" resultMap="cityBase" parameterType="string">
select * from city where countryCode=#{code}
</select>
select * from city 라는 내용이 계속 반복되고 있으므로 이 부분을 모듈화 시켜보자.
<sql id="cityall">
select * from city
</sql>
이제 select * from city 가 필요한 곳에 <include>를 이용해서 삽입시키면 된다.
<select id="select" resultMap="cityBase" parameterType="int">
<!-- select * from city where id=#{id} -->
<include refid="cityall"/>
where id=#{id}
</select>
<select id="selectByCountry" resultMap="cityBase" parameterType="string">
<!-- select * from city where countryCode=#{code} -->
<include refid="cityall"/>
where countryCode=#{code}
</select>
기타
쿼리 캐싱
MyBatis는 동일한 Transaction내에서 조회 쿼리를 캐싱해서 동일한 조회에 대해 이전 조회 결과를 재사용 한다.
예를들어 다음과 같이 트랜젝션 처리 없이 동일한 쿼리를 10번 실행 시키면 10번의 쿼리가 동작한다.
@Test
public void selectTest() {
for (int i = 0; i < 10; i++) {
City city = cRepo.select(2331);
assertEquals(city.getName(), "Seoul");
}
}
21:23:27 [DEBUG] [c.e.d.m.r.C.select.debug-137] > ==> Preparing: select * from city where id=?
21:23:27 [DEBUG] [c.e.d.m.r.C.select.debug-137] > ==> Parameters: 2331(Integer)
21:23:27 [TRACE] [c.e.d.m.r.C.select.trace-143] > <== Columns: ID, Name, CountryCode, District, Population
21:23:27 [TRACE] [c.e.d.m.r.C.select.trace-143] > <== Row: 2331, Seoul, KOR, Seoul, 9981619
21:23:27 [DEBUG] [c.e.d.m.r.C.select.debug-137] > <== Total: 1
-- 8번의 쿼리 실행 로그 생략
21:23:27 [DEBUG] [c.e.d.m.r.C.select.debug-137] > ==> Preparing: select * from city where id=?
21:23:27 [DEBUG] [c.e.d.m.r.C.select.debug-137] > ==> Parameters: 2331(Integer)
21:23:27 [TRACE] [c.e.d.m.r.C.select.trace-143] > <== Columns: ID, Name, CountryCode, District, Population
21:23:27 [TRACE] [c.e.d.m.r.C.select.trace-143] > <== Row: 2331, Seoul, KOR, Seoul, 9981619
21:23:27 [DEBUG] [c.e.d.m.r.C.select.debug-137] > <== Total: 1
하지만 @Transactional을 설정한 후 호출해보면 맨 처음의 쿼리만 동작한다.
@Test
@Transactional
public void selectTest() {
for (int i = 0; i < 10; i++) {
City city = cRepo.select(2331);
assertEquals(city.getName(), "Seoul");
}
}
21:27:51 [DEBUG] [c.e.d.m.r.C.select.debug-137] > ==> Preparing: select * from city where id=?
21:27:51 [DEBUG] [c.e.d.m.r.C.select.debug-137] > ==> Parameters: 2331(Integer)
21:27:51 [TRACE] [c.e.d.m.r.C.select.trace-143] > <== Columns: ID, Name, CountryCode, District, Population
21:27:51 [TRACE] [c.e.d.m.r.C.select.trace-143] > <== Row: 2331, Seoul, KOR, Seoul, 9981619
21:27:51 [DEBUG] [c.e.d.m.r.C.select.debug-137] > <== Total: 1
사실 Transaction으로 묶여 있기 때문에 다른 쿼리가 개입될 여지가 없고 처음 호출의 결과를 재사용하는게 당연히 효율적이다. 그런데 이 사실을 모르고 왜 호출이 안되는지 아주 깊은 삽잘을 한 경험이 있어서 포스팅해놓는다.
만약 중간에 캐쉬된 값을 지우고 싶다면 sqlSession의 clearCache()를 사용하면 된다.
sqlSession.clearCache();
MyBatis에서 paging을 쉽게 처리할 수 있는 pageHelper
'MyBatis' 카테고리의 다른 글
[MyBatis] 06. Enum 타입의 활용 (0) | 2023.06.18 |
---|---|
[MyBatis] 04. 동적 쿼리 (0) | 2023.06.18 |
[MyBatis] 03. 조회 결과의 매핑 (0) | 2023.06.18 |
[MyBatis] 02. CRUD (0) | 2023.06.18 |
[MyBatis] 01. 소개 및 환경 설정 (2) | 2023.06.18 |
소중한 공감 감사합니다