Spring

[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (2) MyBatis

주니어주니 2023. 7. 6. 14:36

 

 

마이바이트 공식 매뉴얼

MyBatis – 마이바티스 3 | 소개

 

마이바티스 스프링 공식 매뉴얼

https://mybatis.org/spring/ko/index.html

 

 

1. JdbcTemplate 과 MyBatis 비교 

 

* SQL이 여러줄일 때 

 

(1) JdbcTemplate 

String sql = "update item " +
    "set item_name=:itemName, price=:price, quantity=:quantity " +
    "where id=:id";

 

(2) MyBatis

<update id="update">
    update item
    set item_name=#{itemName},
        price=#{price},
        quantity=#{quantity}
    where id = #{id}
</update>

 

 

* 동적 쿼리 

 

(1) 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());

 

(2) 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 &lt;= #{maxPrice}
        </if>
    </where>
</select>

 

* MyBatis의 장점 

- 여러 줄일 때 XML 파일에 한번에 작성 가능
- 동적 쿼리

 


 

2. MyBatis 설정 

 

1) MyBatis 라이브러리 추가 

 

* build.gradle

//MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
  • 스프링 부트가 버전을 관리하는 공식 라이브러리 X -> 버전 정보 붙음
  • 스프링 부트가 버전 관리 -> 버전 정보를 붙이지 않아도 최적의 버전을 자동으로 찾아줌

 

 

2) MyBatis 설정 추가 (main, test 각각 추가 !)

 

* main - application.properties 

spring.profiles.active=local
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=

#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug

#MyBatis
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.repository.mybatis=trace

 

* test - application.properties

spring.profiles.active=test
#spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
#spring.datasource.username=sa
#spring.datasource.password=

#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug

#MyBatis
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
    • JdbcTemplate의 BeanPropertyRowMapper처럼 언더바를 카멜로 자동 변경
    • 자바 객체는 카멜형, 데이터베이스는 언더스코어형
      -> item_name으로 조회해도 itemName에 값 저장됨

  • logging.level.hello.itemservice.repository.mybatis=trace
    • MyBatis에서 실행되는 쿼리 로그 확인 

 

 

3. 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);
}
  • 마이바티스 매핑 xml을 호출해주는 매퍼 인터페이스 
  • @Mapper 어노테이션 -> MyBatis에서 인식 가능
  • 메소드 호출 -> xml의 해당 SQL 실행 후 결과 반환

 

* 인터페이스의 구현체가 없는데 동작한 이유

(1) 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈이 @Mapper가 붙어있는 인터페이스 조사

(2) 동적 프록시 기술 사용해서 ItemMapper 인터페이스의 구현체 생성

(3) 생성된 구현체를 스프링 빈으로 등록

-> 프록시가 사용되었는지 Repository에서 확인

 

 

2) ItemMapper.xml - 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 &lt;= #{maxPrice}
            </if>
        </where>
    </select>
</mapper>
  • 패키지 위치를 맞춰줘야 함 - src/main/resources/hello/itemservice/repository/mybatis/ItemMapper.xml
  • namespace : 매퍼 인터페이스 경로 지정
  • xml 파일을 원하는 위치에 두고 싶으면 application.properties 설정
    • mybatis.mapper-locations=classpath:mapper/**/*.xml

 

(1) insert - save

void save(Item item);
<insert id="save" useGeneratedKeys="true" keyProperty="id">
    insert into item (item_name, price, quantity)
    values (#{itemName}, #{price}, #{quantity})
</insert>
  • #{ } : 파라미터 - 매퍼에서 넘긴 객체의 프로퍼티 이름
    -> PreparedStatement 사용해서 JDBC의 ? 를 치환하는 것
  • userGenereatedKeys : 데이터베이스가 키를 생성해주는 IDENTITY 전략일 때 사용
  • keyProperty : 생성되는 키의 속성 이름 지정 

 

(2) update - update

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>
  • 파라미터가 1개 -> @Param 지정하지 않아도 됨
    파라미터가 2개 이상 -> @Param으로 이름을 지정해서 파라미터 구분 (이렇게 안받으면 map으로 만들어서 넘김- 복잡)

 

(3) select - findById

Optional<Item> findById(Long id);
<select id="findById" resultType="Item">
    select id, item_name, price, quantity
    from item
    where id = #{id}
</select>
  • resultType="Item" 
    • mybatis.type-aliases-package=hello.itemservice.domain 지정 -> 패키지명 다 적지 않아도 됨
    • mybatis.configuration.map-underscore-to-camel-case=true 지정 -> 언더스코어에서 카멜형 자동 처리
    • JdbcTemplate의 BeanPropertyRowMapper처럼 select SQL의 결과를 객체로 바로 변환해줌 !

 

(4) select - findAll

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 &lt;= #{maxPrice}
        </if>
    </where>
</select>
  • 편리한 동적 쿼리 지원
    • <if> 
      • 해당 조건이 만족하면 구문 추가

    • <where> 
      • <if>가 모두 실패 -> where 만들지 X
      • <if>가 하나라도 성공 -> 처음 나타나는 and를 where로 변환

  • XML 특수문자
    • XML에서는 데이터 영역에 <, > 사용 X (XML에서 태그가 시작, 종료할 때 <, >를 쓰기 때문)
    • < : &lt;
      > : &gt;
      & : &amp;
    • 또는 CDATA 구문 사용
<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>

 

 

3) MyBatis Repository 

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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Slf4j
@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {

    private final ItemMapper itemMapper;

    @Override
    public Item save(Item item) {
        log.info("itemMapper class={}", itemMapper.getClass());
        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에 기능 위임
  • Mapper 메소드 호출해서 db에 접근

 

* ItemMapper 클래스 출력 (동적 프록시 적용 확인)

itemMapper class=class com.sun.proxy.$Proxy66

-> 실제 ItemMapper 인터페이스가 아니라, 동적 프록시를 주입받은 것 ! 

 

- 매퍼 구현체 : 마이바티스와 스프링 통합, 스프링 예외 추상화 적용, 커넥션 트랜잭션 관련 기능 연동, 동기화

 

 

4) MyBatis Config

  • ItemMapper를 주입받아서 필요한 의존관계 생성
package hello.itemservice.config;

import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV3;
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;

import javax.sql.DataSource;

@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);
    }
}

 

 

6) ItemServiceApplication

  • MyBatisConfig 사용하도록 설정
@Slf4j
@Import(MyBatisConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication { }

 

 


 

4. MyBatis 기능 

 

1) 동적 쿼리 

 

(1) if

  • 내부 문법은 OGNL 사용
<select id="findActiveBlogWithTitleLike" resultType="Blog">
    SELECT * FROM BLOG
    WHERE state = ‘ACTIVE’
    <if test="title != null">
    	AND title like #{title}
    </if>
</select>

 

 

(2) 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>

 

 

(3) where

  • 문장이 없으면 where를 추가하지 X
  • and가 먼저 시작되면 and 제거
<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>

 

trim이라는 기능을 아래와 같이 정의하면 <where>과 같은 기능 수행 

<trim prefix="WHERE" prefixOverrides="AND |OR ">
 ...
</trim>

 

 

(4) foreach

  • 컬렉션 반복 처리
  • where in (1, 2, 3, 4, 5) 같은 문장 완성
<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>

 

 

2) 기타 기능 

 

(1) 어노테이션으로 SQL 작성

@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
  • @Insert, @Update, @Delete, @Select 기능 제공
  • xml에서 해당 쿼리 제거
  • 동적 SQL 해결 X -> 간단한 쿼리만 작성

 

 

(2) 문자열 대체 

@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String 
value);
  • #{ } 문법 - ?를 넣고 파라미터 바인딩하는 PreparedStatement 사용
  • ${ } 문법 - 파라미터 바인딩이 아니라 문자 그대로를 처리하고 싶을 때 
  • 단, SQL 인젝션 공격 당할 수 있음 -> 가급적 사용 X

 

 

(3) 재사용 가능한 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>
  • 프로퍼티 값을 전달할 수 있고, 해당 값을 내부에서 사용 

 

 

(4) ResultMaps

<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>
  • 컬럼명과 객체의 프로퍼티 명이 다를 때 별칭을 사용하지 않고 해결
  • property : 객체의 프로퍼티 명
  • column : DB의 컬럼명