PostgreSQLでtimestamp with time zone型のカラムを使った時に、MyBatis GeneratorでOffsetDateTime型のフィールドにしたい

# What's？

PostgreSQLで`timestamp with time zone`型のカラムを定義して、MyBatis GeneratorでModelを生成すると困ったことになったので、それをなんとかしたいという話です。

具体的には、`timestamp with time zone`型のカラムであればModelのプロパティとしては`OffsetDateTime`型になって欲しいところですが、`LocalDateTime`になってしまいます。

まずは、アプリケーションを作成して確認してみましょう。

# 環境

今回の環境は、こちら。

```shell
$ 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`にしました。

```shell
$ 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
```

使用するテーブルは、以下の定義で作成済みとします。

```sql
create table test_table(
  id integer,
  created timestamp with time zone,
  primary key(id)
);
```

# Spring Bootプロジェクトを作成する

アプリケーションは、Spring Bootプロジェクトとして作成することにします。

```shell
$ 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 -
```

プロジェクト内に移動。

```shell
$ cd spring-boot-mybatis-generator-postgres-timestamptz
```

生成された`pom.xml`の主要設定。

```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>
```

生成されたソースコードは、今回は使わないので削除しておきます。

```shell
$ rm src/main/java/com/example/spring/mybatis/SpringBootMybatisGeneratorPostgresTimestamptzApplication.java src/test/java/com/example/spring/mybatis/SpringBootMybatisGeneratorPostgresTimestamptzApplicationTests.java
```

Spring BootおよびMyBatisの設定は、以下のようにしました。

```properties:src/main/resources/application.properties
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は、以下のように導入。

```xml:pom.xml
        <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:src/main/resources/generatorConfig.xml
<?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を実行。

```shell
$ mvn mybatis-generator:generate
```

生成されたModelを確認してみます。  
※Javadocコメントは省略しています

```java:src/main/java/com/example/spring/mybatis/model/TestTable.java
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`型になってしまっています。

```java
    private LocalDateTime created;
```

MyBatis GeneratorはJDBCの`DatabaseMetaData`を使ってカラムの情報を取得しているのですが、PostgreSQLのJDBCドライバーが`timestamp`型でも`timestamp with time zone`型でも`java.sql.Types#TIMESTAMP`を返してしまうため、このような結果になります。

https://qiita.com/charon/items/0b122f69dca2bde2eae4

MyBatis Generatorにもissueはありますが、「JDBCドライバーの問題だ」ということでクローズされています。

https://github.com/mybatis/generator/issues/860

ちなみに、今回`useJSR310Types`というプロパティを`true`に設定しているものの、これを指定しなくても`java.sql.Types#TIMESTAMP_WITH_TIMEZONE`の場合は`OffsetDateTime`になるのがもともとの挙動のようです。

https://mybatis.org/generator/configreference/javaTypeResolver.html

`java.sql.Types#TIMESTAMP_WITH_TIMEZONE`はPostgreSQLのJDBCドライバーは返さないのですが。

とりあえず、このままにして進めるとどうなるか確認してみます。

Mapperを作成。

```java:src/main/java/com/example/spring/mybatis/mapper/TestTableMapper.java
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`メソッドを持ったクラス。

```java:src/main/java/com/example/spring/mybatis/App.java
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`なので、いったんそれに合わせて実装。トランザクションは有効にしていません（後述）。

```java
    @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());
    }
```

実行。

```shell
$ mvn spring-boot:run
```


すると、データの取得時に例外が発生します。

```shell
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`で扱おうとして失敗しているみたいです。

```shell
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](https://jdbc.postgresql.org/documentation/query/#using-java-8-date-and-time-classes)

ちなみに、`insert`文は`LocalDateTime`のままでも動いたりはします。  
※これを確認（ロールバックされないようにする）ためにトランザクションを使わないことにしました

```sql
example=# select * from test_table;
 id |        created
----+------------------------
  1 | 2022-10-15 18:30:00+09
(1 row)

```

まあ、データが取得できないのはふつうに困りますね。

# columnOverrideを使って対応する型を上書きする

で、どうするかですが。

`OffsetDateTime`を使うという前提に立つ場合、正攻法としてはissueに書かれているとおり`columnOverride`要素の`javaType`属性を使います。

https://github.com/mybatis/generator/issues/860

こちらですね。

[The <columnOverride> Element / Optional Attributes](https://mybatis.org/generator/configreference/columnOverride.html#optional-attributes)

`generatorConfig.xml`の`table`の部分を以下のように修正。カラム単位で対応するJavaの型を指定します。

```xml:src/main/resources/generatorConfig.xml
        <!--
        <table tableName="test_table"/>
        -->
        <table tableName="test_table">
            <columnOverride column="created" javaType="java.time.OffsetDateTime"/>
        </table>
```

再度Modelを生成。

```xml
$ mvn mybatis-generator:generate
```

生成されたModelを確認してみます。

```java:src/main/java/com/example/spring/mybatis/model/TestTable.java
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`が使われるようになりましたね。

```java
    private OffsetDateTime created;
```

当然のことながら`setter`を呼び出している箇所はコンパイルが通らなくなるので、以下のように修正。

```java:src/main/java/com/example/spring/mybatis/App.java
        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")
                )
        );
```

確認。

```shell
$ mvn spring-boot:run
```

今度はうまく動きました。

```shell
id = 1
created = 2022-10-15T09:30Z
```

ここまでで変更したソースコードのうち、一部しか載せていなかった部分を載せておきますね。

```xml:src/main/resources/generatorConfig.xml
<?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>
```

```java:src/main/java/com/example/spring/mybatis/App.java
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`属性で変更できそうですね。

https://mybatis.org/generator/configreference/javaTypeResolver.html

デフォルトの実装クラスは`JavaTypeResolverDefaultImpl`で、`calculateTimestampType`メソッドをオーバーライドすれば良さそうです。

https://github.com/mybatis/generator/blob/mybatis-generator-1.4.1/core/mybatis-generator-core/src/main/java/org/mybatis/generator/internal/types/JavaTypeResolverDefaultImpl.java#L205-L216

ここからは、**timestamp型に対して問答無用でOffsetDateTimeを使う**という`JavaTypeResolver`の実装を作成します。

Mavenプロジェクトをもうひとつ用意。

```xml:pom.xml
<?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`を使うようにしました。

```java:src/main/java/com/example/mybatis/generator/ForceOffsetDateTimeJavaTypeResolver.java
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`。

```shell
$ mvn install
```

元のSpring Bootプロジェクトに戻って、MyBatis Generatorの`dependencies`に作成したモジュールを追加します。

```xml:pom.xml
			<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`属性に指定します。

```xml:src/main/resources/generatorConfig.xml
        <javaTypeResolver type="com.example.mybatis.generator.ForceOffsetDateTimeJavaTypeResolver">
            <property name="useJSR310Types" value="true"/>
        </javaTypeResolver>
```

`columnOverride`は不要になったので、削除します。

```xml:src/main/resources/generatorConfig.xml
        <table tableName="test_table"/>
        <!--
        <table tableName="test_table">
            <columnOverride column="created" javaType="java.time.OffsetDateTime"/>
        </table>
        -->
```

再度Modelを生成。

```shell
$ mvn mybatis-generator:generate
```

`columnOverride`を使った時と同じように、`OffsetDateTime`が使われるようになりました。

```java:src/main/java/com/example/spring/mybatis/model/TestTable.java
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`の実装を変更するやり方もあるというのは覚えておくとよいかもしれません。