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にする方法を考えます。