이번 프로젝트에서 수정사항을 지시 받았는데, 해당 코드를 mybatis로 작성해야 한다고 들었다. 그런데 나는 Spring Data Jpa
, Querydsl
로 데이터 접근을 해왔던지라, mybatis에 대해서는 잘 알지 못한다.
데이터 접근 기술은 실제 데이터베이스에 접근해서 데이터를 잘 저장하고 조회할 수 있는지 확인하는 것이 필요하다.
로컬에서 사용하는 애플리케이션 서버와 테스트에서 같은 데이터베이스를 사용한다면 테스트에서 문제가 발생할 수 있다.
가장 간단한 방법은 테스트 전용 데이터베이스를 별도로 운영하는 것이다.
MyBatis는 SQL을 XML에 편리하게 작성할 수 있고 또 동적 쿼리를 매우 편리하게 작성할 수 있다.
<update id="update">
update item
set item_name=#{itemName},
price=#{price},
quantity=#{quantity}
where id = #{id}
</update>
<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에서 주로 사용하는 기능 위주로 다룰 것이다.
😊 MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
먼저 application.properties
# server port
server.port=8080
#MySQL Connection
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/itDiaLocal?serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=1234
# Logging
logging.level.root=info
logging.level.heelo.itemservice.repository.mybatis=trace
#MyBatis Setting
# 해당 패키지 관련된 부분은 mybatis에서 자동으로 인식해서 파라미터나 응답값에 패키지명을 지정하지 않아도 됨
mybatis.type-aliases-package=com.example.demo
# 데이터베이스에는 underscore가 되어 있어도 객체의 camel-case를 자동으로 해줌./
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.mapper-locations=com/example/demo/repository/mybatis/**/*.xml
#mybatis.configuration.call-setters-on-nulls=true
#참고 - XML 파일 경로 수정하기
#> XML 파일을 원하는 위치에 두고 싶으면 application.properties 에 다음과 같이 설정하면 된다.
#> mybatis.mapper-locations=classpath:mapper/**/*.xml
#>
#> 이렇게 하면 resources/mapper 를 포함한 그 하위 폴더에 있는 XML을 XML 매핑 파일로 인식한다. 이 경우 파일 이름은 자유롭게 설정해도 된다.
#>
#> 참고로 테스트의 application.properties 파일도 함께 수정해야 테스트를 실행할 때 인식할 수 있다.
😓 분석
mybatis.type-aliases-package
mybatis.configuration.map-underscore-to-camel-case
logging.level.hello.itemservice.repository.mybatis=trace
mybatis.mapper-locations
속성은 MyBatis가 SQL 맵핑 XML 파일을 찾는 위치를 지정한다.
classpath
: 접두사는 클래스패스에서 해당 위치를 찾으라는 의미이다.classpath:mapper/**/*.xml
은 클래스패스의 mapper 디렉토리 및 그 하위 디렉토리를 기본으로 한다.반면에, mybatis/mapper/**/*.xml
는 mybatis 디렉토리가 클래스패스의 루트 디렉토리에 있어야 한다는 의미이다. 하지만, 실제로는 mapper 디렉토리가 src/main/resources
아래에 있으므로, 이 설정은 올바르지 않다. 따라서 classpath:mapper/**/*.xml
로 변경해야 한다.
이렇게 함으로써 MyBatis는 src/main/resources/mapper 디렉토리 및 그 하위 디렉토리에서 SQL 맵핑 XML 파일을 찾을 수 있게 된다.
🎈 관례의 불일치
자바 객체에는 주로 카멜( camelCase ) 표기법을 사용한다. itemName 처럼 중간에 낙타 봉이 올라와 있는 표기법이다.
반면에 관계형 데이터베이스에서는 주로 언더스코어를 사용하는 snake_case 표기법을 사용한다.
item_name 처럼 중간에 언더스코어를 사용하는 표기법이다.
이렇게 관례로 많이 사용하다 보니 map-underscore-to-camel-case 기능을 활성화 하면 언더스코어 표기법을 카멜로 자동 변환해준다. 따라서 DB에서 select item_name 으로 조회해도 객체의
itemName ( setItemName() ) 속성에 값이 정상 입력된다.
정리하면 해당 옵션을 켜면 snake_case 는 자동으로 해결되니 그냥 두면 되고, 컬럼 이름과 객체 이름 완전히 다른 경우에는 조회 SQL에서 별칭을 사용하면 된다
이제부터 본격적으로 MyBatis를 사용해서 데이터베이스에 데이터를 저장해보자.
XML에 작성한다는 점을 제외하고는 JDBC 반복을 줄여준다는 점에서 기존 JdbcTemplate과 거의 유사하다.
@Mapper
public interface ItemMapper {
void save(Item item);
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
List<Item> findAll(@Param("itemSearch")ItemSearchCond itemSearch); //파라미터가 하나인 경우에는 @Param을 쓰지 않아도 된다.
Optional<Item> findById(@Param("id" )Long id);
}
src/main/resources/hello/itemservice/repository/mybatis/ItemMapper.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">
<!--위 xml 에 대한 것은 먼저 선언-->
<!--기본 문법은 mapper, 즉 namespace를 지정해줘야 하는데, interface 이름을 지정해줘야해. 패키지명이랑, 인터페이스명을 넣어주기 이러면 연동해줌.,-->
<mapper namespace="com.example.demo.repository.mybatis.ItemMapper">
<!-- insert, update, select 태그들이 있으니 활용하면 됨. -> id에는 Mapper 인터페이스에 설정해뒀던 메서드명을 맞춰줘야 해!-->
<insert id="save" useGeneratedKeys="true" keyProperty="id"> <!-- 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 != ''"> <!-- itemName이 비어있거나, 공백이 아니라면 -->
and item_name like concat('%',#{itemName},'%')
</if>
<if test="maxPrice != null">
and price <= #{maxPrice}
</if>
</where>
</select>
</mapper>
namespace
: 앞서 만든 매퍼 인터페이스를 지정하면 된다.<mapper>
태그의 namespace 속성은 해당 매퍼 XML 파일이 연결되는 매퍼 인터페이스의 완전한 클래스 이름을 지정해야 한다 !!
따라서, namespace 속성의 값은 해당 XML 파일과 연결된 ItemMapper 인터페이스의 완전한 클래스 이름, 즉 com.example.demo.repository.mybatis.ItemMapper와 일치해야 한다.
XML 파일을 원하는 위치에 두고 싶으면
application.properties
에 다음과 같이 설정하면 된다.
mybatis.mapper-locations=classpath:mapper/**/*.xml
이렇게 하면resources/mapper
를 포함한 그 하위 폴더에 있는 XML을 XML 매핑 파일로 인식한다. 이 경우 파일 이름은 자유롭게 설정해도 된다.
참고로 테스트의application.properties
파일도 함께 수정해야 테스트를 실행할 때 인식할 수 있다.
void save(Item item);
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item (item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
Insert SQL은 <insert>
를 사용하면 된다.
id 에는 매퍼 인터페이스에 설정한 메서드 이름을 지정하면 된다. 여기서는 메서드 이름이 save() 이므로 save로 지정하면 된다.
파라미터는 #{}
문법을 사용하면 된다. 그리고 매퍼에서 넘긴 객체의 프로퍼티 이름을 적어주면 된다.
#{} 문법을 사용하면 PreparedStatement 를 사용한다. JDBC의 ? 를 치환한다 생각하면 된다.
useGeneratedKeys
는 데이터베이스가 키를 생성해 주는 IDENTITY 전략일 때 사용한다. keyProperty
는 생성되는 키의 속성 이름을 지정한다. Insert가 끝나면 item 객체의 id 속성에 생성된 값이 입력된다
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>
<update>
를 사용하면 된다.Long id
, ItemUpdateDto updateParam
으로 2개이다. 파라미터가 1개만 있으면 @Param
을 지정하지 않아도 되지만, 파라미터가 2개 이상이면 @Param 으로 이름을 지정해서 파라미터를 구분해야 한다.@Param
어노테이션을 써서 사용하자!!Optional<Item> findById(Long id);
<select id="findById" resultType="Item">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
Select SQL은 <select>
를 사용하면 된다.
resultType
은 반환 타입을 명시하면 된다. 여기서는 결과를 Item 객체에 매핑한다.
앞서 application.properties
에 mybatis.type-aliases-package=hello.itemservice.domain
속성을 지정한 덕분에 모든 패키지 명을 다 적지는 않아도 된다. 그렇지 않으면 모든 패키지 명을 다 적어야 한다.
JdbcTemplate의 BeanPropertyRowMapper 처럼 SELECT SQL의 결과를 편리하게 객체로 바로
변환해준다.
mybatis.configuration.map-underscore-to-camel-case=true
속성을 지정한 덕분에
언더스코어를 카멜 표기법으로 자동으로 처리해준다. ( item_name itemName )
자바 코드에서 반환 객체가 하나이면 Item
, Optional<Item>
과 같이 사용하면 되고, 반환 객체가 하나 이상이면 컬렉션을 사용하면 된다. 주로 List 를 사용한다. 다음을 참고하자.
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>
<where>
, <if>
같은 동적 쿼리 문법을 통해 편리한 동적 쿼리를 지원한다.<if>
는 해당 조건이 만족하면 구문을 추가한다.<where>
은 적절하게 where 문장을 만들어준다.<if>
가 모두 실패하게 되면 SQL where 를 만들지 않는다.<if>
가 하나라도 성공하면 처음 나타나는 and 를 where 로 변환해준다.그런데 가격을 비교하는 조건을 보자
and price <= #{maxPrice}
여기에 보면 <=
를 사용하지 않고 <=
를 사용한 것을 확인할 수 있다. 그 이유는 XML에서는 데이터 영역에 < , > 같은 특수 문자를 사용할 수 없기 때문이다. 이유는 간단한데, XML에서 TAG가 시작하거나 종료할 때 < , > 와 같은 특수문자를 사용하기 때문이다.
< : <
> : >
& : &
다른 해결 방안으로는 XML에서 지원하는 CDATA 구문 문법을 사용하는 것이다. 이 구문 안에서는 특수문자를 사용할 수 있다. 대신 이 구문 안에서는 XML TAG가 단순 문자로 인식되기 때문에 <if>
, <where>
등이 적용되지 않는다.
<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">
<![CDATA[
and price <= #{maxPrice}
]]>
</if>
</where>
</select>