MyBatis

[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

 

 

Spring boot + mybatis + PageHelper

pageHelper 웹 페이지를 만들면서 페이징 처리는 반드시 있어야 하는 내용이다. 하지만 DB마다 다른 쿼리를 사용해야하는 점이나 전체 페이지, 현재 페이지, 페이지당 데이터 수 등을 계산하기가 쉽

goodteacher.tistory.com

 

'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
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.