MyBatisでDateではなくCalendarを利用する方法


MyBatisはJava言語用のORマッパーライブラリです。使い方は以下のようなサイトを参照すると良いと思います。(この投稿の下の方のサンプルコードでも一応分かるかと思います。)

MyBatisでは日付型はDateのみ

MyBatisでDB上のテーブルを操作するとき、テーブルの列が日付型である場合、JavaではDate型を利用します。

Date型は@Deprecatedによって非推奨にされているメソッドが多く、利用を避けたいという人は少なくないと思います。自分としてはCalendarで統一したい所です。過去に関わったプロジェクトでは基準書でDateの利用を禁止している、ということもありました。

MyBatisのAPIを利用する時、Dateを廃止して、Calendarを利用しようとしても残念ながらMyBatisはCalendarを受け付けてくれません。

MyBatisでCalendarを利用してみる(エラー)

例えばMySQLでDATETIME列を持つテーブルを作成し、以下のようなコードで操作を試みます。DateTableクラスの内部にCalendarを持っており、これをDBに挿入したりDBから取り出そうとします。

Main.java

package com.tsukaby.mybatisdate;

import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import java.util.List;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import com.tsukaby.mybatisdate.bean.DateTable;
import com.tsukaby.mybatisdate.mapper.DateTableMapper;

public class Main {

  public static void main(String[] args) {
    System.out.println("Start");

    String resource = "mybatis-config.xml";
    InputStream inputStream = null;
    try {
      inputStream = Resources.getResourceAsStream(resource);
    } catch (IOException e) {
      e.printStackTrace();
    }
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    // 1レコードinsert
    insert(sqlSessionFactory);

    // 全レコードを取得して出力
    selectAndPrint(sqlSessionFactory);

    System.out.println("End");

  }

  private static void insert(SqlSessionFactory sqlSessionFactory) {
    SqlSession session = sqlSessionFactory.openSession();
    try {
      DateTableMapper mapper = session.getMapper(DateTableMapper.class);

      DateTable record = new DateTable();
      Calendar cal = Calendar.getInstance();
      record.setDatetime(cal);
      mapper.insert(record);
      session.commit();
    } finally {
      session.close();
    }
  }

  private static void selectAndPrint(SqlSessionFactory sqlSessionFactory) {
    SqlSession session = sqlSessionFactory.openSession();
    try {
      DateTableMapper mapper = session.getMapper(DateTableMapper.class);

      List<DateTable> list = mapper.selectAll();
      for (DateTable obj : list) {
        System.out.println(obj);
      }
    } finally {
      session.close();
    }
  }
}

DateTable.java

package com.tsukaby.mybatisdate.bean;

import java.io.Serializable;
import java.util.Calendar;

public class DateTable implements Serializable {
  private Calendar datetime;

  private static final long serialVersionUID = 1L;

  public Calendar getDatetime() {
    return datetime;
  }

  public void setDatetime(Calendar datetime) {
    this.datetime = datetime;
  }
}

DateTableMapper.java

package com.tsukaby.mybatisdate.mapper;

import java.util.List;

import com.tsukaby.mybatisdate.bean.DateTable;

public interface DateTableMapper {
  List<DateTable> selectAll();

  int insert(DateTable record);
}

DateTableMapper.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="com.tsukaby.mybatisdate.mapper.DateTableMapper">
  <resultMap id="BaseResultMap" type="com.tsukaby.mybatisdate.bean.DateTable">
    <result column="DATETIME" property="datetime" javaType="java.util.Calendar" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="Base_Column_List">
    DATETIME
  </sql>
  <select id="selectAll" resultMap="BaseResultMap">
    select
    <if test="distinct">
      distinct
    </if>
    <include refid="Base_Column_List" />
    from date_table
  </select>
  <insert id="insert" parameterType="com.tsukaby.mybatisdate.bean.DateTable">
    insert into date_table (DATETIME)
    values (#{datetime,jdbcType=TIMESTAMP})
  </insert>
</mapper>

database.properties

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <properties resource="database.properties" />
  <settings>
    <setting name="logImpl" value="LOG4J" />
  </settings>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>

        <property name="poolPingQuery" value="select 1"/>
        <property name="poolPingEnabled" value="true" /> 

      </dataSource>
    </environment>
  </environments>
  <mappers>
    <package name="com.tsukaby.mybatisdate.mapper"/>
  </mappers>
</configuration>

上記のコードを実行すると、以下のエラーが発生します。

Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: 
### Error building SqlSession.
### The error may exist in com/tsukaby/mybatisdate/mapper/DateTableMapper.xml
### The error occurred while processing mapper_resultMap[BaseResultMap]
### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: org.apache.ibatis.builder.BuilderException: Error parsing Mapper XML. Cause: java.lang.IllegalStateException: No typehandler found for mapping datetime
(以下略)

色々メッセージはありますが、Causeの最も右を参照すると「No typehandler found」と出ています。これは「Calendarのインスタンスをマッピングしようとしたけど、Calendar型に対応したTypeHandlerが無いため、どうマッピングしていいか分からないよ」と言っています。

MyBatisはXMLに定義したとおり、SQLを発行しますが、ここで日付の部分はプレースホルダ―になっています。MyBatisはここに値を埋め込むわけですが、未知の型Calendarからどうやって値を取得して埋め込むべきか分かりません。これを指示するためにTypeHandlerが必要なのです。 (SELECTのときも同様で、DBから取得した値でどのようにCalendarインスタンスを生成すればよいかMyBatisは分かりません)

MyBatisでCalendarを利用してみる(TypeHandlerを利用した解決方法)

どうやってCalendarを利用できるようにするかですが、ここではTypeHandlerを利用します。CalendarTypeHandlerを作成し、mybatis-config.xmlにこれを読み込む設定を行います。

CalendarTypeHandler.java

package com.tsukaby.mybatisdate.typehandler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Calendar;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;

@MappedJdbcTypes(JdbcType.TIMESTAMP)
public class CalendarTypeHandler extends BaseTypeHandler<Calendar> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Calendar cal, JdbcType jt) throws SQLException {
    ps.setTimestamp(i, new Timestamp(cal.getTimeInMillis()));
  }

  @Override
  public Calendar getNullableResult(ResultSet rs, String columnName) throws SQLException {
    Timestamp sqlTimestamp = rs.getTimestamp(columnName);
    if (sqlTimestamp != null) {
      Calendar cal = Calendar.getInstance();
      cal.setTimeInMillis(sqlTimestamp.getTime());
      return cal;
    }
    return null;
  }

  @Override
  public Calendar getNullableResult(ResultSet rs, int i) throws SQLException {
    Timestamp sqlTimestamp = rs.getTimestamp(i);
    if (sqlTimestamp != null) {
      Calendar cal = Calendar.getInstance();
      cal.setTimeInMillis(sqlTimestamp.getTime());
      return cal;
    }
    return null;
  }

  @Override
  public Calendar getNullableResult(CallableStatement cs, int i) throws SQLException {
    Timestamp sqlTimestamp = cs.getTimestamp(i);
    if (sqlTimestamp != null) {
      Calendar cal = Calendar.getInstance();
      cal.setTimeInMillis(sqlTimestamp.getTime());
      return cal;
    }
    return null;
  }

}

mybatis-config.xml (の後に以下を追記します。)

<typeHandlers>
  <package name="com.tsukaby.mybatisdate.typehandler"/>
</typeHandlers>

この状態で実行すると正常に動作します。

動作確認はしていますが、国際化対応する必要があるプログラムの場合、上記のCalendarTypeHandlerのCalendarを操作する部分はよく考えた方が良いかもしれません。(今回は考えないで作ったので注意してください)

このように独自のTypeHandlerを利用すれば大抵の型は何とかなります。MyBatis Generatorを利用している場合はもう少し工夫が必要なのですが、Mapper XMLを自分で作成する場合は今回の手法で問題ないかと思います。次回はMyBatis Generatorで出力するクラスをCalendarにする方法を考えます。