김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 보고 작성한 내용입니다.
https://mybatis.org/mybatis-3/ko/index.html
<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>
...
</where>
</select>
자바에서 SQL을 작성하는 경우, 문자열을 사용하기 때문에 라인을 나눌 때 +
를 사용해야 하지만, MyBatis 는 쿼리를 XML에 편리하게 작성할 수 있고 라인을 나누기 편리합니다.
또한 JdbcTemplate은 조건문을 사용해서 자바 코드로 직접 동적 쿼리를 작성해야하지만, MyBatis 는 xml 태그를 통해 동적 쿼리를 편리하게 작성할 수 있습니다.
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
스프링부트가 버전을 관리해주는 경우 버전 정보를 붙이지 않아도 최적의 버전을 찾아줍니다.
하지만 MyBatis는 스프링부트가 버전을 관리해주는 공식 라이브러리가 아니기 때문에 뒤에 버전 정보를 붙여주어야 합니다
mybatis.configuration.map-underscore-to-camel-case=true mybatis.type-aliases-package=hello.itemservice.domain
map-underscore-to-camel-case
은 DB 에서 사용하는 언더바를 객체에서 사용하는 카멜 표기법으로 자동 변경해주는 기능입니다. ( 디폴트 false )
이 기능으로 인해 DB 에서 item_name 을 조회해도 객체의 itemName 속성에 값이 정상적으로 들어갑니다. 단, 컬럼이름과 객체이름이 다른 경우라면 조회할 때 별칭을 사용하면 됩니다.
1-1 을 보면 resultType
에 객체가 명시되어 있는 것을 볼 수 있습니다. 이처럼 MyBatis 에서 타입 정보를 사용할 때는 패키지 이름을 적어주어야 하는데 위처럼 type-aliases-package
를 지정하여, 쿼리를 작성한 xml 파일에서 패키지 이름을 생략할 수 있습니다.
패키지 이름을 설정하면 지정한 패키지와 그 하위 패키지가 자동으로 인식되어 패키지 이름 없이 객체 이름만 작성해도 사용할 수 있으며, 여러 위치를 지정하려면 ,
또는 ;
으로 구분하면 됩니다.
@Mapper
public interface ItemMapper {
void save(Item item);
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateDto);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond itemSearchCond);
}
MyBatis 매핑 XML을 호출해주는 매퍼 인터페이스입니다. 이때 @Mapper
를 붙여주어야 MyBatis에서 인식할 수 있으며, List
와 Optional
반환도 지원합니다.
파라미터가 한 개인 경우는 상관없지만, 파라미터가 두 개 이상인 경우 @Param
을 붙여야합니다. 이때 사용하는 어노테이션은 ibatis 혹은 mybatis 의 어노테이션을 사용해야 합니다.
xml 태그의 id
속성에 메서드 이름을 적어주면, 매퍼 인터페이스의 메서드 호출 시 해당 id 를 가진 SQL 이 실행되며 결과를 반환합니다.
xml 은 자바코드가 아니기 때문에 resources
하위에 생성합니다. 이때 xml 파일의 위치는 매퍼 인터페이스가 위치한 경로와 동일해야 합니다.
예를 들어, ItemMapper 가 hello.itemservice.repository.mybatis 에 있다면, xml 파일은 resources 하위에 hello.itemservice.repository.mybatis 에 위치해야 합니다.
또한 xml 파일의 이름은 매퍼 인터페이스와 동일한 이름을 가져야합니다. 위에서 ItemMapper 인터페이스를 생성했기 때문에 ItemMapper.xml 을 생성하면 됩니다.
만약 원하는 곳에서 xml 파일을 관리하고 싶으면 properties 에 mybatis.mapper-locations=classpath:mapper/**/*.xml
를 추가하면 됩니다.
위의 설정은 resources/mapper
를 포함한 그 하위 폴더에 있는 XML을 XML 매핑 파일로 인식하도록 하며, 설정을 추가한 경우 파일의 이름은 자유롭게 설정할 수 있습니다.
<!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">
xml 파일에서 위처럼 xml 을 선언합니다. 그리고 mapper
태그를 통해 namespace
를 지정할 수 있는데 이때 매퍼 인터페이스의 경로를 포함한 이름이 지정되어야 합니다.
void save(Item item);
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item(item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
SQL 에서 사용한 `#{itemName}
과 같은 파라미터에는 매퍼에서 넘긴 객체의 프로퍼티 이름과 매칭되어, 값이 바인딩됩니다. 위 예시에서는 파라미터로 넘긴 Item 의 itemName 의 값이 들어갑니다.
DB 가 키를 생성해 주는 IDENTITY 전략일 때는 useGeneratedKeys="true"
를 사용해서 키를 자동으로 생성할 수 있습니다. 이때 keyProperty
를 통해 키 속성의 이름을 지정해야 합니다.
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateDto);
<update id="update">
update item
set item_name = #{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id=#{id}
</update>
매퍼 인터페이스에서 전달하는 파라미터가 한 개라면 위의 insert 문에서 처럼 파라미터 이름 없이 바로 속성 값을 꺼내올 수 있지만, 두 개 이상인 경우 @Param
을 사용해서 이름을 지정하기 때문에 지정된 이름으로 해당 값을 사용해야 합니다.
id 의 경우 지정된 이름이 id 이기 때문에 #{id}
를 사용하였고, updateParam 의 경우 객체이기 때문에 #{updateParam.itemName}
형태로 사용하였습니다.
Optional<Item> findById(Long id);
<select id="findById" resultType="Item">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
조회 쿼리의 경우, 반환타입이 있어야 하기 때문에 resultType
을 지정합니다. 이때 select 의 결과를 resultType 에 지정된 객체로 바로 변환해줍니다.
원래 패키지명까지 함께 적어주어야 하지만 1-3 번 에서 설정 정보에 패키지명을 추가했기 때문에 패지키는 생략해서 사용할 수 있습니다.
List<Item> findAll(ItemSearchCond itemSearchCond);
<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>
동적 쿼리를 작성할 때는 where 와 if 태그를 사용합니다. 이때 if 태그의 test
에 조건을 명시하고, SQL 은 and
로 시작하도록 작성합니다.
모든 if
가 실패하면 where
를 만들지 않고, 처음에 성공한 if 는 and
를 where
로 변환해줍니다.
주의해야 할 점은 위처럼 <=
를 사용할 때 <
를 태그로 인식하기 때문에 <
를 사용합니다. 만약 기호 자체를 사용하고 싶다면 <![CDATA[ ... ]]>
를 사용하면 됩니다.
CDATA 안에서 사용되는 기호는 태그가 아니라 단순 문자로 인식하게 됩니다. 하지만 태그를 문자로 인식한다는 점으로 인해 <if>
나 <where>
가 적용되지 않습니다.
@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {
private final ItemMapper itemMapper;
...
}
@Mapper
가 붙으면 자동으로 프록시 기술을 통해 구현체를 만들어서 스프링 빈에 등록합니다.
그래서 이전에 생성한 매퍼 인터페이스가 스프링 빈으로 등록되고, Repository 에서 의존관계 주입을 통해 사용할 수 있습니다. 이때 위의 Repository 는 주입받은 itemMapper 에 위임하는 기능만 수행합니다.
스프링 빈에는 객체 인스턴스만 등록됩니다. 하지만 Repository 를 보면 ItemMapper 는 인터페이스임에도 주입받아서 사용할 수 있습니다. 이것이 가능한 이유는 MyBatis 스프링 연동 모듈이 자동으로 처리해주기 때문입니다.
어플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper
가 붙어있는 인터페이스를 찾는다
해당 인터페이스를 찾으면 동적 프록시 기술을 사용해서 인터페이스의 구현체를 만든다
생성된 구현체를 스프링 빈으로 등록한다
구현체 조회 결과 :
itemMapper=class com.sun.proxy.$Proxy66
MyBatis 스프링 연동 모듈이 만들어주는 매퍼 인터페이스의 구현체 덕분에 인터페이스만으로 편리하게 XML의 데이터를 찾아서 호출할 수 있습니다.
또 예외 변환까지 처리해주기 때문에 MyBatis 에서 발생한 예외를 스프링 예외 추상화인 DataAccessException 에 맞게 변환해서 반환해줍니다.
참고로 MyBatis 스프링 연동 모듈은 많은 부분을 자동으로 설정해주는데, 데이터베이스 커넥션, 트랜잭션과 관련된 기능도 MyBati s와 함께 연동하고, 동기화해줍니다.
<choose>
<when test="title != null">
AND title like #{title}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
choose 를 사용해서 자바의 switch 구문과 유사한 동적 쿼리를 작성할 수 있습니다. 이때 <when>
을 하나도 만족하지 않으면 <otherwise>
가 실행됩니다.
<where>
<foreach item="item" index="index" collection="list" open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
foreach
는 파라미터로 List 와 같은 컬렉션을 전달했을 때 이를 반복해서 처리해야 하는 경우에 사용합니다.
collection="list"
는 MyBatis 매핑 파일에서 지정한 리스트 객체를 반복하겠다는 것을 의미합니다.
open="ID in ("
은 반복되는 부분의 시작을, separator=","
는 구분자를, close=")"
는 반복되는 부분의 종료를 의미합니다.
결과적으로 해당 동적 쿼리의 결과는 where in( 1, 2, 3 ) 과 같은 형태가 됩니다.
@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
매퍼 인터페이스에 @Insert
, @Update
, @Delete
, @Select
와 같은 어노테이션을 사용해서 쿼리를 작성할 수 있습니다. 이때 xml 에 있는 쿼리는 제거해야 합니다.
하지만 어노테이션을 사용하면 동적 쿼리가 불가능하기 때문에 간단한 경우에만 사용하는 것이 좋다고 합니다.
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
#{ }
문법은 파라미터 바인딩을 수행하는데, 파라미터 바인딩이 아닌 문자 그대로를 출력하고 싶은 경우에는 ${ }
를 사용합니다.
예를 들어, ORDER BY 에서 사용할 컬럼명을 지정하고 싶은 경우에 사용합니다. 하지만 ${}
를 사용하면 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>
2-2-5 와 2-2-6 을 보면 조회 시 사용되는 컬럼이 동일한 것을 볼 수 있습니다. 이러한 경우 컬럼을 별도로 정의해서 include
를 통해 가져올 수 있습니다.
<sql>
을 사용하면 SQL 코드를 재사용 할 수 있는데 여기에 사용할 부분을 정의하고, <include>
를 통해 <sql>
조각을 찾아서 사용합니다.
<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>
결과를 매핑할 때 테이블의 칼럼명과 객체의 필드명이 다른 경우 별칭 as
를 사용합니다. 예를 들어, user_id as id
처럼 사용할 수 있습니다.
이처럼 별칭을 사용하는 방식 말고 resultMap
을 선언해서 사용할 수 있습니다. select 에 resultType 이 아닌 resultMap
을 지정하고, resultMap
의 type 에 객체를 지정합니다.
result
에는 객체의 필드명과 조회하는 컬럼명을 작성하면 됩니다.