MyBatis(마이바티스)는 JdbcTemplate의 단점을 개선할 수 있고 더 많은 기능을 제공하는 SQL Mapper이다. SQL을 XML에 편리하게 작성할 수 있고 동적 쿼리문 또한 XML내에서 더욱 편리하게 작성할 수 있다.
JdbcTemplate 동적 쿼리 적용 모습
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
SqlParameterSource param = new BeanPropertySqlParameterSource(cond);
String sql = "select id, item_name, price, quantity from item";
//동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
// List<Object> param = new ArrayList<>();
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%',:itemName,'%')";
// param.add(itemName);
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= :maxPrice";
// param.add(maxPrice);
}
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());
// param -> 쿼리문 파라미터 바인딩
// rowMapper() -> resultSet -> 객체로 자동변환을 위함이다.
// 결과로, 변화된 Row에 대한 Item들을 List로 반환해준다.
}
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>
// build.gradle (스프링부트 3.0 이상에서는 2.2.0이 아닌 3.0.3 이상 사용)
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
// application.properties
// 추가
mybatis.type-aliases-package=hello.itemservice.domain // 긴 패키지 이름 작성 해소
mybatis.configuration.map-underscore-to-camel-case=true // 관례 불일치 해소
logging.level.hello.itemservice.repository.mybatis=trace // 로그
@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);
}
리포지토리의 CRUD 기능을 구현하기 위해 Mybatis는 매핑 XML을 호출해주기 위한 Mapper 인터페이스가 필요하다. @Mapper를 붙여 이를 인식하게 할 수 있으며 이 인터페이스의 메서드를 호출하면 xml의 해당 sql을 실행하고 결과를 되돌려준다.
<?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" resultType="Item">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
<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>
</mapper>
XML 파일은 매퍼 클래스 파일이 존재하는 경로에 맞추어 디렉토리를 생성하고 매퍼 클래스 파일 이름과 동일하게 생성해야 한다.
현재 나의 실습에서 매퍼 파일의 경로는 다음과 같다.
package hello.itemservice.repository.mybatis;
이 경우 xml파일은 main의 resources에 hello -> itemservice -> repository -> mybatis에 ItemMapper.xml을 만들고 sql문을 등록해야 한다.
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item (item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
// 리포지토리
private final ItemMapper itemMapper;
@Override
public Item save(Item item) {
itemMapper.save(item);
return item;
}
id에는 매퍼 인터페이스에 설정한 메서드 이름을 지정하면 된다. key의 경우 identity전략을 사용하므로 useGeneratedKeys="true"를 주고 key의 속성 이름을 지정하면 Insert 후 item객체의 id속성에 생성된 값이 입력된다. (Jdbc에서의 KeyHolder 로직을 생각하자)
itemMapper.save(item)으로 하여금 xml 내용을 적용시키고, item에 Primary key값을 적재해서 return이 가능해진다. 그러므로 itemMapper 인터페이스의 save 메서드에서 반환타입을 정해야하는 것과 헷갈리면 안된다. 인터페이스 save 메서드의 반환타입은 int로 하여금 영향받은 레코드 개수를 반환받을 수 있는 것이다.
xml에 전달된 객체는 item이므로 item의 getter을 이용하여 MyBatis는 #{필드}를 매핑하여 파라미터 바인딩을 시켜준다.
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto
updateParam);
<update id="update">
update item
set item_name=#{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id = #{id}
</update>
save와 달리 update에서는 파라미터가 두개가 사용되므로 @Param을 통해 이름을 지정해서 구분해야 한다.
Optional<Item> findById(Long id);
<select id="findById" resultType="Item">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
위의 update, save와 달리 findById 메서드는 리턴값을 받고 있다 . 이 경우 태그에 resultType="Item"와 같이 명시한다면 결과를 Item 객체에 매핑한다. Jdbc에서의 RowMapper 기능을 자동 수행해준다고 생각하면 된다. 우리가 이전에 설정에서 부여했던 mybatis.configuration.map-underscore-to-camel-case=true로 하여금 언더스코어를 카멜로 자동 처리해준다.
List<Item> findAll(ItemSearchCond itemSearch);
<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>
동적 쿼리문을 구성했던 findAll 메서드의 경우 where태그를 이용한다 이 where태그는 if문이 모두 탈락할 경우 where절을 만들지 않으며 하나라도 성공할 경우 앞에 있는 and를 where로 변환해서 적용해준다.
한 가지 의문점은 우리는 @Mapper 애노테이션이 달린 ItemMapper 인터페이스를 구현체 없이 그대로 사용한다는 점이다. 이것이 가능한 이유는 MyBatis가 애플리케이션 로딩 시점에 @Mapper가 붙어있는 인터페이스를 조사한 후 동적 프록시 기술을 이용하여 ItemMapper 인터페이스의 구현체를 자동으로 만들어 낸다. 생성된 구현체를 스프링 빈으로 등록하므로 우리는 프록시 기술로 생성된 보이지 않는 구현체를 주입받아 사용하게 되는 것이다.