본문 바로가기
Spring/DB 2편

데이터 접근 기술 - MyBatis

by JHyun0302 2023. 8. 10.
728x90

JdbcTemplate - SQL 여러줄

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

 

 

 

MyBatis - SQL 여러줄

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

 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";
}

log.info("sql={}", sql);
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 &lt;= #{maxPrice}
        </if>
    </where>
</select>

 

 

 

설정의 장단점

  • JdbcTemplate : 별도의 설정없이 사용 가능 (스프링에 내장된 기능)
  •  MyBatis는 약간의 설정이 필요

 

 

 

 

 

 

 

 


MyBatis 설정

 

build.gradle

//MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'

 

 

 

 

application.properties (main & test)
#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

▶ JdbcTemplateBeanPropertyRowMapper 에서 처럼 언더바를 카멜로 자동 변경해주는 기능을 활성화

 

logging.level.hello.itemservice.repository.mybatis=trace 

MyBatis에서 실행되는 쿼리 로그를 확인할 수 있다.

 

 

 

 


기본

 

 

ItemMapper

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

 

  • 이 인터페이스에는 @Mapper 애노테이션을 붙여야 MyBatis에서 인식 가능
  • 메서드를 호출시 xml 의 해당 SQL을 실행하고 결과를 돌려준다.

 

◎ 참고 : 자바 코드가 아니기 때문에 src/main/resources 하위에 만들되, 패키지 위치는 맞추어 주어야 한다.

 

 

 

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">
    
<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>

 

★ namespace : 앞서 만든 매퍼 인터페이스를 지정

 

 

 

 

 

참고 : XML 파일 경로 수정하기 (application.properties 수정)

mybatis.mapper-locations=classpath:mapper/**/*.xml

 

 

 

 

 

XML 특수문자

  < : &lt;
  > : &gt;
  & : &amp;

 

 

 

 

 

XML에서 지원하는 CDATA 구문 문법을 사용

대신 이 구문 안에서는 XML TAG가 단순 문자로 인식 되기 때문에 <if>, <where> 등이 적용되지 않는다.

 

XML 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>

 

 

 

 

 

 


분석

 

ItemMapper 인터페이스

@Mapper
public interface ItemMapper {
    void save(Item item);
    void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
    List<Item> findAll(ItemSearchCond itemSearch);
    Optional<Item> findById(Long id);
}

 

→ 인터페이스의 구현체가 없어도 MyBatis 스프링 연동 모듈에서 자동으로 처리한다.

 

 

 

 

 

 

설정 원리

 

  1. 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper 가 붙어있는 인터페이스를 조사한다.
  2. 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해서 ItemMapper 인터페이스의 구현체를 만든다.
  3. 생성된 구현체를 스프링 빈으로 등록한다.

 

 

 

※ 정리

  • 매퍼 구현체를 사용하면 스프링 예외 추상화도 함께 적용된다.
  • 마이바티스 스프링 연동 모듈이 데이터베이스 커넥션, 트랜잭션과 관련된 기능도 마이바티스와 함께 연동하고, 동기화해준다.

 

 

 

◎ 참고
마이바티스 스프링 연동 모듈이 자동으로 등록 : MybatisAutoConfiguration 클래스를 참고

 

 

 

 

 

 


동적 쿼리

 

 

 

 

동적 SQL

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

 

 

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 구문과 유사한 구문도 사용

 

 

 

 

 

trim, where, set

 

<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> 는 문장이 없으면 where 를 추가 안하고 문장이 있으면 where 를 추가한다.
  • 만약 and 가 먼저 시작된다면 and 를 지운다.
  • trim 이라는 기능으로 사용해도 된다. <where> 와 같은 기능을 수행
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    ...
</trim>

 

 

 

 

 

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 를 전달

 

 

 

 

 

참고 : 동적 쿼리에 대한 자세한 내용은 다음을 참고하자.
https://mybatis.org/mybatis-3/ko/dynamic-sql.html

 

 

 

 

 


기타 기능

 

 

 

애노테이션으로 SQL 작성

@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);

 

  • @Insert , @Update , @Delete , @Select 기능이 제공된다.
  • XML에는 <select id="findById"> ~ </select> 는 제거해야 한다.
  • 동적 SQL이 해결되지 않으므로 간단한 경우에만 사용

 

 

◎ 애노테이션으로 SQL 작성에 대한 더 자세한 내용은 다음을 참고

 https://mybatis.org/mybatis-3/ko/java-api.html

 

 

 

 

문자열 대체(String Substitution)


#{} 문법은 ?를 넣고 파라미터를 바인딩하는 PreparedStatement 를 사용한다.
때로는 파라미터 바인딩이 아니라 문자 그대로를 처리하고 싶은 경우도 있다. 이때는 ${} 를 사용하면 된다.

@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

 

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>

 

  • 프로퍼티 값을 전달할 수 있고, 해당 값은 내부에서 사용

 

 

 

 

 

 

 

 


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>

 

  • 결과를 매핑할 때 테이블은 user_id 이지만 객체는 id 이다.
  • 컬럼명과 객체의 프로퍼티 명이 다르다. 그러면 다음과 같이 별칭( as )을 사용하면 된다.

 

 

<resultMap id="userResultMap" type="User">
    <id property="id" column="user_id" />
    <result property="username" column="username"/>
    <result property="password" column="password"/>
</resultMap>
  
<select id="selectUsers" resultMap="userResultMap">
    select user_id, user_name, hashed_password
    from some_table
    where id = #{id}
</select>

 

  • 별칭을 사용하지 않고도 문제를 해결할 수 있는데, 다음과 같이 resultMap 을 선언해서 사용하면 된다.

 

 

 

 

◎ 참고 : 결과 매핑에 대한 자세한 내용은 다음을 참고
https://mybatis.org/mybatis-3/ko/sqlmap-xml.html#Result_Maps

 

반응형