MyBatis
Mybatis는 JdbcTemplate보다 더 많은 기능을 제공하는 SQL Mapper이다. 기본적으로 JdbcTemplate이 제공하는 기능을 대부분 제공하며, XML과 동적 쿼리를 매우 편리하게 작성할 수 있다는 장점이 있다.
MyBatis vs JdbcTemplate
JdbcTemplate과 MyBatis의 코드 차이를 알아보자.
JdbcTemplate
String sql = "update item " +
"set item_name=:itemName, price=:price, quantity=:quantity " +
"where id=:id";
MyBatis -SQL
<update id="update">
update item
set item_name=#{itemName},
price=#{price},
quantity=#{quantity}
where id = #{id}
</update>
- XML을 기반으로 라인이 길어져도 문자 더하기에 대한 불편함이 없다.
JdbcTemplate - 동적 쿼리
String sql = "select id, item_name, price, quantity from item"; //동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= :maxPrice";
}
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());
MyBatis - 동적 쿼리
<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>
- 동적 쿼리를 편리하게 작성할 수 있는 기능을 제공한다.
JdbcTemplate은 스프링에 내장된 기능이고, 별도의 설정없이 사용할 수 있다는 장점이 있는 반면에 동적 쿼리 적용이 복잡하다. MyBatis는 동적 쿼리를 매우 편리하게 작성할 수 있다는 장점이 있으나, 별도의 설정이 필요하다는 단점이 있다.
MyBatis 설정
build.gradle
//MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
- build.gradle에
mybatis-spring-boot-starter
라이브러리를 주입하면 스프링과 통합되고, 설정도 간단히 할 수 있다. 참고로 스프링 부트에서 제공해주는 라이브러리는 버전 관리를 자동적으로 해주지만 해당 라이브러리의 경우 스프링의 라이브러리가 아니기 때문에 버전을 명시해서 주입해야 한다.
main, test - application.properties
#MyBatis
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.repository.mybatis=trace
- type-aliases-package
- 마이바티스에서 타입 정보를 사용할 때는 패키지 이름을 적어주어야 하는데, 여기에 명시하면 패키지 이름을 생략할 수 있다.
- 지정한 패키지와 그 하위 패키지가 자동으로 인식된다.
- 여러 위치를 지정하려면
,
,;
로 구분하면 된다.
- configuration.map-underscore-to-camel-case
- JdbcTemplate의
BeanPropertyRowMapper
처럼 스네이크를 카멜케이스로 자동 변경을 해주는 기능을 활성화 한다. - 참고로 DB 컬럼과 애플리케이션의 속성이 카멜,스네이크 차이가 아닌 아예 다른 경우에는 애플리케이션 레벨에서 직접 alias를 쿼리문에 적용시켜 해결한다.
- JdbcTemplate의
MyBatis 적용 1 - 기본
ItemMapper
package hello.itemservice.repository.mybatis;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Optional;
@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 매핑 XML을 호출해주는 매퍼 인터페이스를 생성하자. 이 인터페이스에는
@Mapper
를 설정해줘야 MyBatis에서 인식할 수 있다. - 해당 인터페이스의 메서드를 호출하면 메스드명을 통해 xml의 해당 SQL을 실행하고 결과를 반환한다.
@Param
은 파라미터가 1개일 때는 생략해도 되지만, 2개 이상이면 반드시 지정해서 파라미터를 구분해줘야 한다.
ItemMapper.xml
이제 실행 SQL이 있는 XML 매핑 파일을 만들어주면 된다. 여기서 주의할 점은 자바를 제외한 다른 파일들은 resources
에 보관하는게 원칙이다. 다음과 같이 디렉토리를 동일하게 맞추어서 xml 파일을 생성해야 한다.
- hello/itemservice/repository/mybatis 를 동일하게 만들어서 생성
<?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">
<mapper namespace="hello.itemservice.repository.mybatis.ItemMapper">
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item (item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
<update id="update">
update item
set item_name=#{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id = #{id}
</update>
<select id="findById">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
<select id="findAll">
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>
</mapper>
namespace
: 앞서 만든 매퍼 인터페이스의 경로를 지정한다.- 파라미터는
#{}
로 지정한다. 이 문법은 PrepareStatement를 사용해서 바인딩 해준다. useGeneratedKeys
는 데이터베이스 키 생성이 IDENTITY 전략일 때 사용한다.<
는<
를 의미하며 XML에서 특수 문자를 표시하기 위해 사용한다. (XML은 태그로 인식하기 때문)>
:>
,&
:&
등이 있다.
<where>
,<if>
와 같은 동적 쿼리 문법을 통해 동적 쿼리를 사용할 수 있다. if가 모두 실패하면 where를 만들지 않고 하나라도 성공하면 처음 나타나는and
를where
로 변환해서 적용시킨다.
XML CDATA 적용
<if test="maxPrice != null">
<![CDATA[
and price <= #{maxPrice}
]]>
</if>
- 다음과 같이 CDATA를 사용하면 특수 문자를 XML 내부에서 사용할 수 있다.
참고 - XML 파일 경로 수정하기
XML 파일을 원하는 위치에 두고 싶으면 application.properties에 설정을 통해 적용할 수 있다.
mybatis.mapper-locations=classpath:mapper/**/*.xml
과 같이 resources/mapper
를 포함한 그 하위 폴더에 있는 XML을 매핑 파일로 인식한다.
MyBatis 적용 2 - 설정과 실행
MyBatisItemRepository
package hello.itemservice.repository.mybatis;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@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);
}
}
- ItemMapper 인터페이스를 단순히 위임하는 구현체 레포지토리를 생성하자.
MyBatisConfig
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.mybatis.ItemMapper;
import hello.itemservice.repository.mybatis.MyBatisItemRepository;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class MyBatisConfig {
private final ItemMapper itemMapper;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new MyBatisItemRepository(itemMapper);
}
}
- ItemMapper를 주입 받아, 필요한 의존관계를 빈 등록하자.
ItemServiceApplication
@Import(MyBatisConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
- 컨트롤러만 스캔하므로 별도의 설정파일을 등록하자.
MyBatis 설정 원리
- 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은
@Mapper
가 붙어있는 인터페이스를 조사한다. - 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해서
ItemMapper
인터페이스 구현체를 만든다. - 생성된 구현체를 스프링 빈으로 등록한다.
MyBatis 기능 정리 1 - 동적 쿼리
마이바티스를 사용하는 이유는 바로 동적 SQL 기능 때문이다. if, choose (when, otherwise), trim(where, set), foreach 등 여러 기능을 제공한다.
자세한 내용은 공식 문서를 참고하자. https://mybatis.org/mybatis-3/ko/dynamic-sql.html
if
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
- 해당 조건에 따라 값 추가 여부를 판단한다. 내부 문법은 OGNL을 사용한다.
choose, when, otherwise
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
- switch 구문과 비슷하게 구문을 사용할 수 있다.
trim, where, set
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
- 이 예제는 문장을 모두 만족하지 않을 때 쿼리문의 문제가 발생한다. 즉, 사용하지 않는다.
where 태그 사용
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
- where 태그를 사용하면 만족하는 하위 조건이 없으면 추가하지 않고, 있으면 첫 번째 AND가 WHERE로 변환 된다.
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>
- 컬렉션을 반복 처리할 때 사용한다.파라미터로 List를 전달하여 사용한다.
MyBatis 기능 정리 2 - 기타 기능
애노테이션으로 SQL 작성
@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
@Insert
,@Update
,@Delete
,@Select
등의 기능을 제공한다.- 이 경우 XML에는 crud 태그를 제거해야 한다. 동적 SQL이 해결되지 않으므로 간단한 경우에 사용한다.
문자열 대체
#{}
문법은 ?를 넣고 파라미터를 바인딩하는 PrepareStatement
를 사용한다. 때로는 파라미터 바인딩이 아니라 문자 그대로 처리하고 싶은 경우도 있다. 이 때는 ${}
를 사용하면 된다.
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String
${}
를 사용하면 SQL 인젝션 공격을 당할 수 있기 때문에 가급적 사용하면 안된다.
재사용 가능한 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 조각을 찾아 사용할 수 있다.
Result Maps
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="username"/>
<result property="password" column="password"/>
</resultMap>
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>
- 별칭(as)를 사용하고 싶을 때
resultMap
을 선언해서 사용할 수 있다.
복잡한 결과매핑
MyBatis도 복잡한 결과에 객체 연관관계를 고려해서 데이터를 조회하는 것이 가능하다.
이 때 association, collection 등의 태그를 사용하지만, 성능과 실효성 측면에서 불리한 면이 많기 때문에 고민이 필요하다.
REFERENCES
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/