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
(</settings>の後に以下を追記します。)
<typeHandlers> <package name="com.tsukaby.mybatisdate.typehandler"/> </typeHandlers>
この状態で実行すると正常に動作します。
動作確認はしていますが、国際化対応する必要があるプログラムの場合、上記のCalendarTypeHandlerのCalendarを操作する部分はよく考えた方が良いかもしれません。(今回は考えないで作ったので注意してください)
このように独自のTypeHandlerを利用すれば大抵の型は何とかなります。MyBatis Generatorを利用している場合はもう少し工夫が必要なのですが、Mapper XMLを自分で作成する場合は今回の手法で問題ないかと思います。次回はMyBatis Generatorで出力するクラスをCalendarにする方法を考えます。