[Spring] MyBatis 사용법 익히기 !
MyBatis 소개
- JdbcTemplate 보다 많은 기능을 제공하는 SQL Mapper 중 하나이다.
- SQL 을 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";
}
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>
초기 설정
build.gradle 에 다음 설정을 추가해 준다.
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
(MyBatis 는 스프링 부트가 버전 관리해 주는 공식 라이브러리가 아니기 때문에, 뒤에 버전 정보가 붙는다.)
`main` 과 `test` 의 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.configuration.map-underscore-to-camel-case` : snake_case 와 camelCase 표시법을 자동 변경하는 기능을 활성화
`logging.level.hello.itemservice.repository.mybatis=trace` : 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);
}
- MyBatis 매핑 XML 을 호출해 주는 매퍼 인터페이스이다.
- 파라미터가 2개 이상이면 @Param 을 붙여야 한다.
- `@Mapper` 애노테이션을 붙여야 MyBatis 에서 인식할 수 있다.
- 메서드를 호출하면 매핑 XML 의 해당 SQL 을 실행하고 결과를 돌려준다.
> 참고 <
- 매퍼 인터페이스의 구현체는 Mybatis 스프링 모듈이 자동으로 인식하여 생성하고, 빈으로 등록해 준다.
XML 매핑 파일
<?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>
- src/main/resources 하위에 패키지 위치를 맞추어 만들어 준다.
- `namespace` 에 매퍼 인터페이스를 지정한다.
- `#{}` : `PreparedStatement` 를 사용한다.
Insert
- `useGeneratedKeys` : 데이터베이스가 키를 생성해 주는 `IDENTITY` 전략일 때 사용한다.
- `keyProperty` : 생성되는 키의 속성 이름을 지정한다. Insert 가 끝나면, id 속성에 생성된 값이 입력된다.
Select
- `resultType` : 결과를 해당 반환 타입의 객체로 매핑하여 반환한다.
(`mybatis.type-aliases-package` 속성으로 패키지 명을 생략할 수 있다.)
(`mybatis.configuration.map-underscore-to-camel-case` 속성으로 표기법 간의 차이를 자동으로 처리해 준다.)
XML 특수문자
데이터 영역에 특수 문자를 사용할 수 없기 때문에, `<`, `>` 같은 문자로 치환하여 사용한다.
하지만 CDATA 구문 문법을 사용하면 해당 구문 안에서는 특수문자를 사용할 수 있다.
<if test="maxPrice != null">
<![CDATA[
and price <= #{maxPrice}
]]>
</if>
리포지토리
@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);
}
}
매퍼 인터페이스의 @Mapper 를 통해 Mybatis 스프링 연동 모듈에서 매퍼 인터페이스(ItemMapper) 를 자동으로 인식한다.
그리고 매퍼 구현체로 동적 프록시 객체를 자동으로 만들고 빈으로 등록해 주기 때문에, 리포지토리에서 매퍼 인터페이스의 구현체를 의존관계 주입받아 사용할 수 있다.
매퍼 구현체
MyBatis 스프링 연동 모듈이 만들어주는 구현체 덕분에 인터페이스만으로 편리하게 XML 데이터를 찾아 호출할 수 있다.
또한 스프링 예외 추상화 기능도 제공하여 DataAccessException 에 맞게 변환하여 반환해 준다.
MyBatis 스프링 연동 모듈
- 데이터베이스 커넥션
- 트랜잭션
등 수많은 부분을 자동 설정해 준다.
MyBatis 동적 SQL 기능
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 구문과 유사하게 사용할 수 있다.
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 __ 같은 경우를 동적으로 처리해 준다.
trim
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
- 다음과 같이 정의하면, <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>
- where in (1, 2, 3, 4, 5, 6) 과 같은 구문에 컬렉션을 반복 처리할 때 사용한다.
- 파라미터로 List 를 전달하면 된다.
MyBatis 기타 기능
@Insert, @Update, @Delete, @Select
@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
- XML 대신 애노테이션에 직접 SQL 을 작성할 수 있다.
- 간단한 경우에만 사용한다.
문자열 대체 ${ }
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
- 파라미터 바인딩이 아닌 문자 그대로 처리하고 싶은 경우에 사용한다.
- SQL 인젝션 공격에 주의해야 한다. (가급적 사용 X)
재사용 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>
- 프로퍼티 값을 전달할 수도 있고, 해당 값은 내부에서 사용할 수 있다.
(<sql> 의 SQL 문에서 ${ } 값이, <property> 의 name 값과 같으면 value 값을 전달한다.)
Result Maps
<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>
- 칼럼명과 객체의 프로퍼티 명이 다른 경우 별칭 (as) 를 사용한다.
(snake_case 와 camelCase 표시법 간의 변환으로 해결되지 않을 때)
<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>
- 별칭을 사용하지 않고 해결할 수 있다.
출처 | 스프링 DB 2(김영한) - 인프런