MyBatis 사용하기 [기초]
MyBatis에서는 기본적으로 JdbcTemplate이 제공하는 대부분의 기능을 제공한다.
추가적으로 아래와 같은 장점을 가지고있다.
- SQL을 XML을 통해 편리하게 작성, 관리
- 동적 쿼리를 태그로 편리하게 작성
설정JdbcTemplate
은 스프링에 내장된 기능으로 별도의 설정없이 사용할 수 있다.
반면에 MyBatis
는 약간의 설정이 필요하다.
실무에서는 프로젝트에 동적쿼리와 복잡한 쿼리가 많다면 MyBatis를 사용하고 단순한 쿼리들이 많다면 JdbcTemplate을 선택해서 사용하면된다.
물론 이 둘을 함께 사용할수도 있다.
MyBatis공식 사이트
https://mybatis.org/mybatis-3/ko/index.html
동적 쿼리에 대한 문서
https://mybatis.org/mybatis-3/ko/dynamic-sql.html
설정
Gradle기준 의존성 주입implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
스프링 부트 3.0버전 이상일경우implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
application.properties
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.repository.mybatis=trace
mybatis.type-aliases-package
- MyBatis를 사용할 때 XML파일에서 타입정보를 패키지 이름을 입력해줌으로써 XML에서 간단하게 타입정보를 입력할 수 있다.
mybatis.configuration.map-underscore-to-camel-case
JdbcTemplate
의BeanPropertyRowMapper
에서처럼 스네이크 케이스를 카멜케이스로 자동변경해주는 기능을 활성화한다.
logging.level.hello.itemservice.repository.mybatis=true
- MyBatis에서 실행되는 쿼리 로그를 확인할 수 있다.
MyBatis 사용하기
\
매퍼 인터페이스
@Mapper
public interface ItemMapper {
void save(Item item);
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond itemSearch);
}
먼저 XML파일에서 매핑될 인터페이스를 선언해주어야한다.
- 매퍼 인터페이스에는 반드시
@Mapper
어노테이션을 붙여주어야하며, 이 어노테이션을 기반으로 MyBatis에서 인식한다. - 매퍼 인터페이스의 메서드가 호출되면 XML파일에 선언되어있는 해당되는 SQL을 실행하고 결과를 반환받는다.
[!매퍼 인터페이스의 구현체는 내부적으로 생성] Title
Contents
XML파일
실행할 SQL을 담고있는 XML파일을 작성해주어야한다.
참고로 XML파일은 resource
하위 디렉터리에 만들어져야하며 패키지는 매퍼 인터페이스와 맞춰주어야한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
MyBatis를 사용하기위한 기본 선언이라고 생각하자
<mapper namespace="hello.itemservice.repository.mybatis.ItemMapper">
</mapper>
사용할 SQL의 범위를 정의하며 해당 태그안에 사용할 SQL들을 작성해 주어야한다.
namespace
속성은 매퍼 인터페이스의 패키지경로를 지정해주면 된다.- 만약 XML을 따로 패키지를 만들어 관리하고싶다면
application.properties
파일에mybatis.mapper-locations=classpath:mapper/**/*.xml
로 설정해주면resources/mapper
를 포함한 그 하위 폴더에 있는 XML을 XML매핑 파일로 인식한다.
save()
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item (item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
id
속성은 매퍼 인터페이스에 정의되어있는 실행할 메서드의 이름을 지정해준다.- 파라미터 값은
#{}
문법을 통하여 작성해주며 매퍼 인터페이스에서 넘긴 객체에 맞추어준다. userGenerateKeys
속성은 데이터베이스가 키를 생성해주는 IDENTITY전략일 때 사용한다.keyProperty
는 생성되는 키의 속성 이름을 지정하므로Item
객체의id
필드에 생성된 값이 입력된다.
update()
Mapper Interface
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto
updateParam);
XML
<update id="update">
update item
set item_name=#{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id=#{id}
</update>
매퍼 인터페이스에서 update()
메서드의 파라미터는 id
값과 UpdateDto
두 개의 값을 받았다.
파라미터가 두 개 이상일 경우에는 @Param
으로 XML에서 사용될 파라미터의 이름을 지정해주어서 파라미터를 구분해주어야한다.
findById()
<select id="findById" resultType="Item">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
resultType
속성은 반환 타입을 명시한다. 해당되는 SQL의 실행결과를resultType
에 맞게 매핑해준다. 위 예시에서는Item
객체에 매핑해준다.application.properties
에서mybatis.type-aliases-package
설정을 설정해준 덕분에resultType
속성 값에Item
객체를 명시하는 것만으로 간단하게 사용할 수 있다.JdbcTemplate
의BeanPropertyRowMapper
처럼 SQL의 결과를 편리하게 객체로 바로 변경해준다(ResultSet
)application.properties
에서map-underscore-to-camler-case=true
로 설정해줬기 때문에 item_name컬럼을itemName
으로 자동으로 치환해준다.
findAll()
Mapper 인터페이스
List<Item> findAll(ItemSearchCond itemSearch);
findAll
의 실행결과는 여러 Row가 반환될 수 있기때문에 List
컬렉션으로 반환타입을 받아준다.
XML
<select id="findAll" resultType="Item">
select id, item_name, price, quantity
from item
<where>
<if test="itemName != null and itemName != ''">
and item_name like concat('%',#{itemName},'%')
</if>
<if test="maxPrice != null">
and price <= #{maxPrice}
</if>
</where>
</select>
MyBatis
를 사용하는 이유인 동적쿼리를 처리하는 기능이다.
<where>, <if>와 같은 태그형태로 동적 쿼리 문법을 편리하게 제공할 수 있다.
- \ <if>는 해당 조건이 만족한다면 바디 부분의 구문을 추가한다.
- \ <where>는 조건에 맞게 동적으로 where절을 처리한다.
- 만약 <if>가 모두 실패하게 되면 SQL where를 추가하지 않는다. (추가되면 SQL문법오류)
- 만약 <if>가 하나라도 성공하면 처음나타나는
and
를where
로 변환해준다.
- 추가로
and price <= #{maxPrice}
에서 <= 를 직접사용할 경우 태그로 인식되기 때문에 <와 같이 특수문자를 변형해서 사용해야한다.
[!resultType에 기재한 객체와 매핑 되지않을경우]
SQLException
또는TypeMismatchException
예외가 발생할 수 있다.- 예상치 못한 데이터가 반환될 수 있다.
즉, SQL결과가resultType
에 기재한 객체와 제대로 매핑되어야하며 다음 사항을 준수해야한다- SQL결과 셋의 컬럼 이름이
resultType
객체의 필드이름과 일치해야한다.- SQL결과 셋의 데이터 타입이
resultType
객체의 필드 타입과 호환되도록 해야한다- 매핑을 명시적으로 설정하는 어노테이션을 제공하기도한다 아래 코드와 같다.
@Select("select id, item_name, price, quantity from item where id = #{id}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "itemName", column = "item_name"),
@Result(property = "price", column = "price"),
@Result(property = "quantity", column = "quantity")
})
Item findById(Long id);
되도록이면 사용하지 말고, 참고만 해두자
MyBatis Repository계층 구현
@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {
private final ItemMapper itemMapper;
@Override
public Item save(Item item) {
itemMapper.save(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
itemMapper.update(itemId, updateParam);
}
@Override
public Optional<Item> findById(Long id) {
return itemMapper.findById(id);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
return itemMapper.findAll(cond);
}
}
MyBatis를 사용함으로써 Repository
계층에서는 단순히 매퍼 인터페이스를 구현한 구현 클래스(MyBatis가 내부적으로 생성한 구현클래스)에 기능을 위임한다.
결과적으로는 Repository
계층에 SQL이 들어가지 않게된다.
매퍼 인터페이스의 구현체
지금까지 우리가 매퍼 인터페이스인 ItemMapper
를 사용할 때 이에 대한 구현 클래스가 없음에도 마치 구현체가 있는듯이 사용되었던 것을 확인할 수 있었다.
이에 대해 MyBatis가 내부적으로 어떤 처리를하고 어떻게 동작하는지 살펴보자
먼저 매퍼 인터페이스는 아래와 같았다
@Mapper
public interface ItemMapper {
void save(Item item);
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond itemSearch);
}
- 먼저 의존성 주입을 통해 받은 라이브러리인 MyBatis스프링 연동 모듈에서
@Mapper
를 조회한다. @Mapper
가 사용된 인터페이스를 발견하면 동적 프록시 기술을 사용해서ItemMapper
인터페이스의 구현체를 생성한다.- 생성된 구현체를 스프링 빈으로 등록한다.
- 마이바티스 스프링 연동 모듈덕분에 매퍼 인터페이스 만으로도 편리하게 XML을 사용할 수 있다.
- 매퍼 구현체는 예외 변한까지 처리해준다.
- 마이바티스 스프링 연동 모듈은 데이터베이스 커넥션, 트랜잭션과 관련된 기능도 마이바티스와 함께 연동하고 동기화해준다.
MyBatis의 기능
MyBatis 공식 메뉴얼: https://mybatis.org/mybatis-3/ko/index.html
MyBatis 스프링 공식 메뉴얼: https://mybatis.org/spring/ko/index.html
동적 쿼리
마이바티스는 XML의 특징을 살려 동적쿼리를 편리하게 사용할수 있도록 한다.
동적 쿼리를 위해 제공되는 기능(태그)은 다음과 같다
- if
- choose(when, otherwise) - java의 switch와 유사
- trim(where, set)
- foreach
<where>
<mapper namespace="ItemMapper">
<select id="findByCondition" resultType="Item">
select id, item_name, price, quantity
from item
<where>
<if test="itemName != null and itemName != ''">
and item_name = #{itemName}
</if>
<if test="price != null">
and price = #{price}
</if>
</where>
</select>
</mapper>
- 사용자가 필터검색란에서 상품명과 가격제한을 두지 않았을 경우
select id, item_name, price, quantity from item
SQL이 실행- 모든
item
데이터베이스를 출력한다
- 사용자가 상품명에 대한 입력이 있을 경우
select Id, item_name, price, quantity from item
에 추가로and item_name = #{itemName}
이 들어와 상품명에 대한 조건을 추가한다.- 만약 이 조건이 처음으로 들어올 경우 and가 where로 변환된다.
- 사용자가 가격에 대한 입력이 있을 경우
select Id, item_name, price, quantity from item
에 추가로and price = #{price}
가 추가된다.- 이 조건도 마찬가지로 처음 조건으로 들어갈 경우 and가 where로 변환된다.
- 상품명, 가격에 대한 입력이 모두 있을 경우 아래와 같이 동적SQL이 만들어진다
select id, item_name, price, quantity from item where item_name = #{itemName} and price = #{price}
<foreach>
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
<where>
<foreach item="item" index="index" collection="list"
open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
</select>
- 컬렉션을 반복 처리할 때 사용하며
where in (1, 2, 3, 4, 5, 6)
과 같은 문장을 쉽게 완성한다.
<sql>
<sql>을 사용하면 SQL코드를 재사용 할 수 있다.
<sql id="userColumns">
${alias}.id,${alias}.username,${alias}.password
</sql>
<select id="selectUsers" resultType="map">
select
<include refid="userColumns">
<property name="alias" value="t1"/>
</include>,
<include refid="userColumns">
<property name="alias" value="t2"/>
</include>
from some_table t1
cross join some_table t2
</select>
<include>
를 통해서 <sql>
조각을 찾아서 사용할 수 있다.
<sql id="sometable">
${prefix}Table
</sql>
<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
<include refid="someinclude">
<property name="prefix" value="Some"/>
<property name="include_target" value="sometable"/>
</include>
</select> ```
프로퍼티 값을 전달할 수 있고, 해당 값은 내부에서 사용할 수 있다.
**Result Maps**
결과를 매핑할 때 테이블은 `user_id` 이지만 객체는 `id` 이다.
이 경우 컬럼명과 객체의 프로퍼티 명이 다르다. 그러면 다음과 같이 별칭( `as` )을 사용하면 된다.
```xml
<select id="selectUsers" resultType="User">
select
user_id as "id",
user_name as "userName",
hashed_password as "hashedPassword"
from some_table
where id = #{id}
</select>
위와 같이 별칭을 사용할수도있지만, resultMap을 선언해서 간편하게 사용할수도있다.
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>
결과매핑에 대한 문서
https://mybatis.org/mybatis-3/ko/sqlmap-xml.html#Result_Maps