mybatis (2)

MyBatis TypeHandler를 이용한 객체리스트 핸들링

이번에 특정 서비스를 개발하다가 JSON 형태의 스트링을 그대로 데이터베이스에 저장했다가 꺼내써야하는 상황이 생겼다.

그것도 RBD인 mysql 데이터베이스에 말이다. mysql이 json 컬럼을 지원하기는 하지만 json 함수를 쿼리에 쓸 필요까지는 없는 상황이다보니 그냥 varchar타입으로 컬럼을 정의했고 여기에 json포맷의 스트링을 그대로 저장했다가 꺼내쓸 수 있도록 해야했다.

일단 타입핸들러를 이용하여 이를 처리하기로 했고 어떻게 사용하는 건지 검색을 좀 해봤다. 구글에서 "Type handler for ArrayList in myBatis" 라고 검색을 하니 많은 포스팅이 검색이 되었고 내 프로젝트에 맞게 가져다가 쓸 수 있었다.

작업 내용을 정리하자면 다음과 같다. 

 

우선 dto의 구조를 보면 MyJsonDataWrapperClass 가 myJsonData 리스트를 들고 있는데 이때 MyJsonData 클래스가 바로 json 형태로 데이터베이스에 저장될 정보이다.

@Getter
@Setter
@JsonInclude(Include.NON_NULL)
public class MyJsonDataWrapperClass {

    private Integer someIntData;
    private List<MyJsonData> myJsonData = new ArrayList<>();
    
    @Override
    public String toString() {
        return new Gson().toJson(this);
    }
}
@Getter
@Setter
@EqualsAndHashCode
public class MyJsonData {
    private String name;
    private int age;

    @Override
    public String toString() {
        return new Gson().toJson(this);
    }
}

 

데이터베이스에는 myJsonData 라는 이름으로 text타입의 컬럼을 만들었고 mybatis의 insert문에 사용할 타입핸들러를 아래와 같이 명시해주었다.

, myJsonData = #{myJsonData, typeHandler=MyJsonDataTypeHandler}

 

그리고 타입핸들러는 아래와 같이 정의하였다.

@Slf4j
public class MyJsonDataTypeHandler extends BaseTypeHandler<List<MyJsonData>> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, List<MyJsonData> parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, new Gson().toJson(parameter));
    }

    @Override
    public List<MyJsonData> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return convertToList(rs.getString(columnName));
    }

    @Override
    public List<MyJsonData> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return convertToList(rs.getString(columnIndex));
    }

    @Override
    public List<MyJsonData> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return convertToList(cs.getString(columnIndex));
    }

    private List<MyJsonData> convertToList(String myJsonDataListAsString) {
        try {
            return new ObjectMapper().readValue(myJsonDataListAsString, new TypeReference<List<MyJsonData>>() {
            });
        } catch (IOException e) {
            log.error("MyJsonDataTypeHandler failed to convert text to list, myJsonDataListAsString:{}", myJsonDataListAsString, e);
        }
        return Collections.emptyList();
    }
}

 

BaseTypeHandler를 상속하면서 List<MyJsonData> 타입에 대한 핸들러임을 명시해주었고 setter에는 리스트 형태를 json 스트링으로 만들어주도록 Gson.toJson() 메서드를 이용하였고, getter에는 조회한 json 스트링을 다시 리스트 형태로 변환해주도록 하였는데 이때는 ObjectMapper.readValue()를 이용하였다.

 

조회시에는 mybatis 매핑파일에 아래와 같이 resultMap을 정의하여 MyJsonDataTypeHandler를 이용해서 myJsonData필드의 값을 ArrayList로 변환하도록 하였으며, 이렇게 정의한 resultMap을 select 문의 resultMap으로 선언해주었다.

<resultMap id="myJsonDataClassMap" type="MyJsonDataWrapperClass">
<result property="myJsonData" column="myJsonData" javaType="java.util.ArrayList"
jdbcType="VARCHAR" typeHandler="MyJsonDataTypeHandler" />
</resultMap>

....중략....

<select id="findMyJsonDataType"
resultMap="myJsonDataClassMap">
SELECT * FROM findMyJsonDataWrapperClass
</select>

 

이렇게 해주면 소스레벨에서는 특정 객체의 리스트 형태로 핸들링을 하면서 데이터베이스에는 json 스트링으로 저장하여 사용할 수 있다.

 

컬럼단위로 데이터를 쪼개서 저장하기 애매한 상황에서 json 스트링을 varchar(text) 타입으로 통으로 저장하고 소스레벨에서는 객체타입으로 핸들링할 수 있도록 해주는 건 정말 멋진 기능인것 같다.

 

 

MyBatis에서 전달받은 객체(MyObject)의 필드에 특정 값을 세팅할 때 selectKey 태그를 이용할 수 있다.


예를들어 MyObject가 아래 두 개의 필드를 가지고 있는 클래스라고 가정하자.

String id;

String name;


MyBatis를 이용하여 MyObject를 데이터베이스에 입력하고 싶을 때 이름값만 화면에서 전달받고 ID를 자동으로 sequence를 따서 입력 해줄 수 있으며, 이때  MyObject의 id필드에 값 세팅


<insert id="insert" parameterType="MyObject">

        <selectKey keyProperty="id" resultType="int" order="BEFORE">

            select nextval('sequence_id')

        </selectKey>

        <![CDATA[

        INSERT INTO person(id, name)

        VALUES (#{id}, #{name});

        ]]>

    </insert>


order="BEFORE" 옵션은 insert 쿼리를 실행하기전에 keyProperty에 값을 세팅하라는 의미이다.


자바 소스단에서 객체의 id에 값을 세팅하지 않고있는데 쿼리 실행 후에 id값이 세팅되어져서 나오는 케이스가 있었는데 이게 뭔가 싶어 찾아보다가 알게됨.


mybatis설정 파일에는 userGeneratedKeys 설정이 있어야 한다.

<configuration>

    <settings>

        <setting name="cacheEnabled" value="false"/>

        <setting name="useGeneratedKeys" value="true"/>

        <setting name="defaultExecutorType" value="REUSE"/>

    </settings>

</configuration>




위 처럼 사용할 수도 있으나 저렇게 사용하는 케이스가 많지 않다면 쿼리에 직접 옵션으로 줄 수도 있다.


<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

속성에 대한 설명은 아래와 같다.

설명
id구문을 찾기 위해 사용될 수 있는 네임스페이스내 유일한 구분자
parameterType구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭
parameterMap외부 parameterMap 을 찾기 위한 비권장된 접근방법. 인라인 파라미터 매핑과 parameterType을 대신 사용하라.
flushCache이 값을 true 로 셋팅하면 구문이 호출될때마다 캐시가 지원질것이다(flush). 디폴트는 false 이다.
timeout예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대시간을 설정한다. 디폴트는 셋팅하지 않는 것이고 드라이버에 따라 다소 지원되지 않을 수 있다.
statementTypeSTATEMENT, PREPARED 또는 CALLABLE중 하나를 선택할 수 있다. 마이바티스에게 Statement, PreparedStatement 또는 CallableStatement를 사용하게 한다. 디폴트는 PREPARED 이다.
useGeneratedKeys(입력(insert, update)에만 적용) 데이터베이스에서 내부적으로 생성한 키 (예를들어 MySQL또는 SQL Server와 같은 RDBMS의 자동 증가 필드)를 받는 JDBC getGeneratedKeys메소드를 사용하도록 설정하다. 디폴트는 false 이다.
keyProperty(입력(insert, update)에만 적용) getGeneratedKeys 메소드나 insert 구문의 selectKey 하위 엘리먼트에 의해 리턴된 키를 셋팅할 프로퍼티를 지정. 디폴트는 셋팅하지 않는 것이다. 여러개의 칼럼을 사용한다면 프로퍼티명에 콤마를 구분자로 나열할수 있다.
keyColumn(입력(insert, update)에만 적용) 생성키를 가진 테이블의 칼럼명을 셋팅. 키 칼럼이 테이블이 첫번째 칼럼이 아닌 데이터베이스(PostgreSQL 처럼)에서만 필요하다. 여러개의 칼럼을 사용한다면 프로퍼티명에 콤마를 구분자로 나열할수 있다.
databaseId설정된 databaseIdProvider가 있는 경우 마이바티스는 databaseId 속성이 없는 모든 구문을 로드하거나 일치하는 databaseId와 함께 로드될 것이다. 같은 구문에서 databaseId가 있거나 없는 경우 모두 있다면 뒤에 나온 것이 무시된다.