What's?
PostgreSQLでtimestamp with time zone
型のカラムを定義して、MyBatis GeneratorでModelを生成すると困ったことになったので、それをなんとかしたいという話です。
具体的には、timestamp with time zone
型のカラムであればModelのプロパティとしてはOffsetDateTime
型になって欲しいところですが、LocalDateTime
になってしまいます。
まずは、アプリケーションを作成して確認してみましょう。
環境
今回の環境は、こちら。
$ java --version
openjdk 17.0.4 2022-07-19
OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing)
$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: /home/charon/.sdkman/candidates/maven/current
Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-128-generic", arch: "amd64", family: "unix"
PostgreSQLは、Dockerコンテナで用意。タイムゾーンはAsia/Tokyo
にしました。
$ docker container run -it --rm -p 5432:5432 \
-e POSTGRES_DB=example \
-e POSTGRES_USER=charon \
-e POSTGRES_PASSWORD=password \
-e TZ=Asia/Tokyo \
--name postgres \
postgres:14.5
使用するテーブルは、以下の定義で作成済みとします。
create table test_table(
id integer,
created timestamp with time zone,
primary key(id)
);
Spring Bootプロジェクトを作成する
アプリケーションは、Spring Bootプロジェクトとして作成することにします。
$ curl -s https://start.spring.io/starter.tgz \
-d bootVersion=2.7.4 \
-d javaVersion=17 \
-d name=spring-boot-mybatis-generator-postgres-timestamptz \
-d groupId=com.example \
-d artifactId=spring-boot-mybatis-generator-postgres-timestamptz \
-d version=0.0.1-SNAPSHOT \
-d packageName=com.example.spring.mybatis \
-d dependencies=mybatis,postgresql \
-d baseDir=spring-boot-mybatis-generator-postgres-timestamptz | tar zxvf -
プロジェクト内に移動。
$ cd spring-boot-mybatis-generator-postgres-timestamptz
生成されたpom.xml
の主要設定。
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
生成されたソースコードは、今回は使わないので削除しておきます。
$ rm src/main/java/com/example/spring/mybatis/SpringBootMybatisGeneratorPostgresTimestamptzApplication.java src/test/java/com/example/spring/mybatis/SpringBootMybatisGeneratorPostgresTimestamptzApplicationTests.java
Spring BootおよびMyBatisの設定は、以下のようにしました。
spring.datasource.url=jdbc:postgresql://localhost:5432/example
spring.datasource.username=charon
spring.datasource.password=password
mybatis.configuration.map-underscore-to-camel-case=true
MyBatis Generatorを使ってModelを作成して、とりあえず動かしてみる
では、まずはふつうにMyBatis Generatorを導入してアプリケーションを作り、動かしてみましょう。
MyBatis Generatorは、以下のように導入。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<configuration>
<configurationFile>${project.basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<includeAllDependencies>true</includeAllDependencies>
</configuration>
</plugin>
</plugins>
</build>
MyBatis Generatorの設定。生成するのは、Modelのみにしています。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC
"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="tables" targetRuntime="MyBatis3">
<plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"/>
<plugin type="org.mybatis.generator.plugins.MapperAnnotationPlugin"/>
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<jdbcConnection
driverClass="org.postgresql.Driver"
connectionURL="jdbc:postgresql://localhost:5432/example"
userId="charon"
password="password"/>
<javaTypeResolver>
<property name="useJSR310Types" value="true"/>
</javaTypeResolver>
<javaModelGenerator
targetPackage="com.example.spring.mybatis.model"
targetProject="src/main/java"/>
<table tableName="test_table"/>
</context>
</generatorConfiguration>
MyBatis Generatorを実行。
$ mvn mybatis-generator:generate
生成されたModelを確認してみます。
※Javadocコメントは省略しています
package com.example.spring.mybatis.model;
import java.time.LocalDateTime;
public class TestTable {
private Integer id;
private LocalDateTime created;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public LocalDateTime getCreated() {
return created;
}
public void setCreated(LocalDateTime created) {
this.created = created;
}
〜省略〜
}
timestamp with time zone
型のカラムに対応するプロパティが、LocalDateTime
型になってしまっています。
private LocalDateTime created;
MyBatis GeneratorはJDBCのDatabaseMetaData
を使ってカラムの情報を取得しているのですが、PostgreSQLのJDBCドライバーがtimestamp
型でもtimestamp with time zone
型でもjava.sql.Types#TIMESTAMP
を返してしまうため、このような結果になります。
MyBatis Generatorにもissueはありますが、「JDBCドライバーの問題だ」ということでクローズされています。
ちなみに、今回useJSR310Types
というプロパティをtrue
に設定しているものの、これを指定しなくてもjava.sql.Types#TIMESTAMP_WITH_TIMEZONE
の場合はOffsetDateTime
になるのがもともとの挙動のようです。
java.sql.Types#TIMESTAMP_WITH_TIMEZONE
はPostgreSQLのJDBCドライバーは返さないのですが。
とりあえず、このままにして進めるとどうなるか確認してみます。
Mapperを作成。
package com.example.spring.mybatis.mapper;
import com.example.spring.mybatis.model.TestTable;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TestTableMapper {
@Insert("insert into test_table(id, created) values(#{id}, #{created})")
int insert(TestTable testTable);
@Select("select id, created from test_table where id = #{id}")
TestTable selectById(Integer id);
@Delete("truncate table test_table")
int truncate();
}
main
メソッドを持ったクラス。
package com.example.spring.mybatis;
import java.time.LocalDateTime;
import com.example.spring.mybatis.mapper.TestTableMapper;
import com.example.spring.mybatis.model.TestTable;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App implements ApplicationRunner {
TestTableMapper testTableMapper;
public App(TestTableMapper testTableMapper) {
this.testTableMapper = testTableMapper;
}
public static void main(String... args) {
SpringApplication.run(App.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
testTableMapper.truncate();
TestTable testTable = new TestTable();
testTable.setId(1);
testTable.setCreated(LocalDateTime.of(2022, 10, 15, 18, 30, 0));
testTableMapper.insert(testTable);
TestTable selected = testTableMapper.selectById(1);
System.out.printf("id = %d%n", selected.getId());
System.out.printf("created = %s%n", selected.getCreated());
}
}
生成された型がLocalDateTime
なので、いったんそれに合わせて実装。トランザクションは有効にしていません(後述)。
@Override
public void run(ApplicationArguments args) throws Exception {
testTableMapper.truncate();
TestTable testTable = new TestTable();
testTable.setId(1);
testTable.setCreated(LocalDateTime.of(2022, 10, 15, 18, 30, 0));
testTableMapper.insert(testTable);
TestTable selected = testTableMapper.selectById(1);
System.out.printf("id = %d%n", selected.getId());
System.out.printf("created = %s%n", selected.getCreated());
}
実行。
$ mvn spring-boot:run
すると、データの取得時に例外が発生します。
Caused by: org.springframework.jdbc.BadSqlGrammarException: Error attempting to get column 'created' from result set. Cause: org.postgresql.util.PSQLException: TIMESTAMPTZ型のカラムの値を指定の型 java.time.LocalDateTime に変換できませんでした。
; bad SQL grammar []; nested exception is org.postgresql.util.PSQLException: TIMESTAMPTZ型のカラムの値を指定の型 java.time.LocalDateTime に変換できませんでした。
at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:101) ~[spring-jdbc-5.3.23.jar:5.3.23]
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70) ~[spring-jdbc-5.3.23.jar:5.3.23]
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79) ~[spring-jdbc-5.3.23.jar:5.3.23]
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79) ~[spring-jdbc-5.3.23.jar:5.3.23]
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:91) ~[mybatis-spring-2.0.7.jar:2.0.7]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441) ~[mybatis-spring-2.0.7.jar:2.0.7]
at jdk.proxy2/jdk.proxy2.$Proxy46.selectOne(Unknown Source) ~[na:na]
at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:160) ~[mybatis-spring-2.0.7.jar:2.0.7]
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86) ~[mybatis-3.5.9.jar:3.5.9]
at jdk.proxy2/jdk.proxy2.$Proxy51.selectById(Unknown Source) ~[na:na]
at com.example.spring.mybatis.App.run(App.java:36) ~[classes/:na]
at com.example.spring.mybatis.App$$FastClassBySpringCGLIB$$41d58896.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.23.jar:5.3.23]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793) ~[spring-aop-5.3.23.jar:5.3.23]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.23.jar:5.3.23]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.23.jar:5.3.23]
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.23.jar:5.3.23]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.23.jar:5.3.23]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.23.jar:5.3.23]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.23.jar:5.3.23]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.23.jar:5.3.23]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.23.jar:5.3.23]
at com.example.spring.mybatis.App$$EnhancerBySpringCGLIB$$adaa3109.run(<generated>) ~[classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:762) ~[spring-boot-2.7.4.jar:2.7.4]
... 5 common frames omitted
Caused by: org.postgresql.util.PSQLException: TIMESTAMPTZ型のカラムの値を指定の型 java.time.LocalDateTime に変換できませんでした。
at org.postgresql.jdbc.PgResultSet.getLocalDateTime(PgResultSet.java:727) ~[postgresql-42.3.7.jar:42.3.7]
at org.postgresql.jdbc.PgResultSet.getObject(PgResultSet.java:3760) ~[postgresql-42.3.7.jar:42.3.7]
at org.postgresql.jdbc.PgResultSet.getObject(PgResultSet.java:3780) ~[postgresql-42.3.7.jar:42.3.7]
at com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java) ~[HikariCP-4.0.3.jar:na]
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:38) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:28) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:85) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:561) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:403) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:355) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:329) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:302) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:195) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140) ~[mybatis-3.5.9.jar:3.5.9]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76) ~[mybatis-3.5.9.jar:3.5.9]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:427) ~[mybatis-spring-2.0.7.jar:2.0.7]
... 25 common frames omitted
timestamp with time zone
型のカラムをLocalDateTime
で扱おうとして失敗しているみたいです。
Caused by: org.postgresql.util.PSQLException: TIMESTAMPTZ型のカラムの値を指定の型 java.time.LocalDateTime に変換できませんでした。
PostgreSQLのJDBCドライバーとしても、OffsetDateTime
で扱うべきなんですよね。
Issuing a Query and Processing the Result / Using Java 8 Date and Time classes
ちなみに、insert
文はLocalDateTime
のままでも動いたりはします。
※これを確認(ロールバックされないようにする)ためにトランザクションを使わないことにしました
example=# select * from test_table;
id | created
----+------------------------
1 | 2022-10-15 18:30:00+09
(1 row)
まあ、データが取得できないのはふつうに困りますね。
columnOverrideを使って対応する型を上書きする
で、どうするかですが。
OffsetDateTime
を使うという前提に立つ場合、正攻法としてはissueに書かれているとおりcolumnOverride
要素のjavaType
属性を使います。
こちらですね。
The Element / Optional Attributes
generatorConfig.xml
のtable
の部分を以下のように修正。カラム単位で対応するJavaの型を指定します。
<!--
<table tableName="test_table"/>
-->
<table tableName="test_table">
<columnOverride column="created" javaType="java.time.OffsetDateTime"/>
</table>
再度Modelを生成。
$ mvn mybatis-generator:generate
生成されたModelを確認してみます。
package com.example.spring.mybatis.model;
import java.time.OffsetDateTime;
public class TestTable {
private Integer id;
private OffsetDateTime created;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public OffsetDateTime getCreated() {
return created;
}
public void setCreated(OffsetDateTime created) {
this.created = created;
}
〜省略〜
}
OffsetDateTime
が使われるようになりましたね。
private OffsetDateTime created;
当然のことながらsetter
を呼び出している箇所はコンパイルが通らなくなるので、以下のように修正。
TestTable testTable = new TestTable();
testTable.setId(1);
//testTable.setCreated(LocalDateTime.of(2022, 10, 15, 18, 30, 0));
testTable.setCreated(OffsetDateTime.of(
LocalDateTime.of(2022, 10, 15, 18, 30, 0),
ZoneOffset.of("+09:00")
)
);
確認。
$ mvn spring-boot:run
今度はうまく動きました。
id = 1
created = 2022-10-15T09:30Z
ここまでで変更したソースコードのうち、一部しか載せていなかった部分を載せておきますね。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC
"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="tables" targetRuntime="MyBatis3">
<plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"/>
<plugin type="org.mybatis.generator.plugins.MapperAnnotationPlugin"/>
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<jdbcConnection
driverClass="org.postgresql.Driver"
connectionURL="jdbc:postgresql://localhost:5432/example"
userId="charon"
password="password"/>
<javaTypeResolver>
<property name="useJSR310Types" value="true"/>
</javaTypeResolver>
<javaModelGenerator
targetPackage="com.example.spring.mybatis.model"
targetProject="src/main/java"/>
<!--
<table tableName="test_table"/>
-->
<table tableName="test_table">
<columnOverride column="created" javaType="java.time.OffsetDateTime"/>
</table>
</context>
</generatorConfiguration>
package com.example.spring.mybatis;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import com.example.spring.mybatis.mapper.TestTableMapper;
import com.example.spring.mybatis.model.TestTable;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App implements ApplicationRunner {
TestTableMapper testTableMapper;
public App(TestTableMapper testTableMapper) {
this.testTableMapper = testTableMapper;
}
public static void main(String... args) {
SpringApplication.run(App.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
testTableMapper.truncate();
TestTable testTable = new TestTable();
testTable.setId(1);
//testTable.setCreated(LocalDateTime.of(2022, 10, 15, 18, 30, 0));
testTable.setCreated(OffsetDateTime.of(
LocalDateTime.of(2022, 10, 15, 18, 30, 0),
ZoneOffset.of("+09:00")
)
);
testTableMapper.insert(testTable);
TestTable selected = testTableMapper.selectById(1);
System.out.printf("id = %d%n", selected.getId());
System.out.printf("created = %s%n", selected.getCreated());
}
}
JavaTypeResolverのデフォルトの挙動を変更したい
columnOverride
を使ってカラム単位でデータ型を変更していくのが正攻法ではあるのですが、個々のカラムに対して設定が必要なので正直めんどうというか、ミスが起こりそうで嫌です…。
全体の挙動を変更する設定はなさそうなので、org.mybatis.generator.api.JavaTypeResolver
の実装を変更するしか内容がします。
type
属性で変更できそうですね。
デフォルトの実装クラスはJavaTypeResolverDefaultImpl
で、calculateTimestampType
メソッドをオーバーライドすれば良さそうです。
ここからは、timestamp型に対して問答無用でOffsetDateTimeを使うというJavaTypeResolver
の実装を作成します。
Mavenプロジェクトをもうひとつ用意。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mybatis-generator-custom-type-resolver</artifactId>
<version>0.0.1</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
calculateTimestampType
メソッドをオーバーライドして、OffsetDateTime
を使うようにしました。
package com.example.mybatis.generator;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl;
public class ForceOffsetDateTimeJavaTypeResolver extends JavaTypeResolverDefaultImpl {
@Override
protected FullyQualifiedJavaType calculateTimestampType(IntrospectedColumn column,
FullyQualifiedJavaType defaultType) {
FullyQualifiedJavaType answer;
if (useJSR310Types) {
answer = new FullyQualifiedJavaType("java.time.OffsetDateTime");
} else {
answer = defaultType;
}
return answer;
}
}
一応、useJSR310Types
は見るようにしています(有効にしない場合はjava.sql.Timestamp
型にマッピングされます)。
こちらをmvn install
。
$ mvn install
元のSpring Bootプロジェクトに戻って、MyBatis Generatorのdependencies
に作成したモジュールを追加します。
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<configuration>
<configurationFile>${project.basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<includeAllDependencies>true</includeAllDependencies>
</configuration>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>mybatis-generator-custom-type-resolver</artifactId>
<version>0.0.1</version>
</dependency>
</dependencies>
</plugin>
MyBatis Generatorの設定を変更し、先ほど作成したクラスをjavaTypeResolver
要素のtype
属性に指定します。
<javaTypeResolver type="com.example.mybatis.generator.ForceOffsetDateTimeJavaTypeResolver">
<property name="useJSR310Types" value="true"/>
</javaTypeResolver>
columnOverride
は不要になったので、削除します。
<table tableName="test_table"/>
<!--
<table tableName="test_table">
<columnOverride column="created" javaType="java.time.OffsetDateTime"/>
</table>
-->
再度Modelを生成。
$ mvn mybatis-generator:generate
columnOverride
を使った時と同じように、OffsetDateTime
が使われるようになりました。
package com.example.spring.mybatis.model;
import java.time.OffsetDateTime;
public class TestTable {
private Integer id;
private OffsetDateTime created;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public OffsetDateTime getCreated() {
return created;
}
public void setCreated(OffsetDateTime created) {
this.created = created;
}
〜省略〜
}
アプリケーションの実行結果は、先ほどと同じになるので省略します。
ケースバイケースかつ、ちょっと乱暴なのであんまりやりたくはないですが、JavaTypeResolver
の実装を変更するやり方もあるというのは覚えておくとよいかもしれません。