mike-neckのブログ

Java or Groovy or Swift or Golang

Java8のDate and Time API - JPAでLocalDateTimeを扱う

こんにちわ、みけです。

最近、ずっとJava8のDate and Time APIをいじってきましたが、

多分、これでおわりです。

今日は新APIと古いAPIの相互変換についてです。

java.time.Instant

APIと古いAPIでの相互変換はjava.time.Instantを経由して行います。

@RunWith(Enclosed.class)
public class DateTimeTest {
    //ゾーンなしフォーマット
    private static final DateTimeFormatter withoutZone = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
    //ゾーンつきフォーマット
    private static final DateTimeFormatter withZone = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss Z");
    //東京
    private static final ZoneId ASIA_TOKYO = ZoneId.of("Asia/Tokyo");
    //GMT
    private static final ZoneId GMT = ZoneId.of("GMT");
    //日時(ゾーン込み)
    private static final String DATE_TIME = "2014/10/24 14:09:20 +0900";
    //メッセージをキューから出力するクラス
    private synchronized static void log(Queue<Output> outputs) {/* 省略 */}
    //メッセージとメソッド名とクラス名を文字列に連結するクラス
    private static class Output {/* 省略 */}

    public static class DateとLocalDateTimeの相互変換 {

        private static final String CLASS = ZonedDateTimeとLocalDateTimeの相互変換.class.getSimpleName();
        private static final Queue<Output> QUEUE = new ConcurrentLinkedQueue<>();
        //日時(Date用)
        private static final String DATE = "2014/10/24 14:09:20";
        //フォーマット(Date用)
        private static final String pattern = "yyyy/MM/dd HH:mm:ss";

        @Rule
        public TestName testName = new TestName();

        @Test
        public void DateからLocalDateTimeへはInstantを経由して変換() throws ParseException {
            Date dateType = new SimpleDateFormat(pattern).parse(DATE);
            QUEUE.offer(
                    new Output("Date          [" + new SimpleDateFormat(pattern).format(dateType) + ']',
                            CLASS, testName));
            Instant instant = dateType.toInstant();
            LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ASIA_TOKYO);
            QUEUE.offer(new Output("LocalDateTime [" + withoutZone.format(localDateTime) + ']', CLASS, testName));
        }

        @Test
        public void LocalDateTimeからDateへもInstantを経由して変換() throws ParseException {
            LocalDateTime localDateTime = LocalDateTime.parse(DATE_TIME, withZone);
            QUEUE.offer(new Output("LocalDateTime [" + withoutZone.format(localDateTime) + ']', CLASS, testName));
            Instant instant = localDateTime.toInstant(ZoneOffset.ofHours(9));
            Date dateType = Date.from(instant);
            QUEUE.offer(
                    new Output("Date          [" + new SimpleDateFormat(pattern).format(dateType) + ']',
                            CLASS, testName));
        }
        //キューのメッセージを出力
        @AfterClass
        public static void outputMessages() {
            log(QUEUE);
        }
    }
}

最初のテストDateからLocalDateTimeへはInstantを経由して変換では、

DateからLocalDateTimeへの変換が行われます。

DateからInstantを取り出すメソッドDate#toInstantです。

InstantからLocalDateTimeに変換するメソッド

LocalDateTime#ofInstant(Instant, ZoneId)です。

これによって、DateからLocalDateTimeに変換できます。

出力は次のとおりです。

Date          [2014/10/24 14:09:20]
LocalDateTime [2014/10/24 14:09:20]

次のテストLocalDateTimeからDateへもInstantを経由して変換は、

最初のテストの逆で、LocalDateTimeからDateへの変換を行います。

LocalDateTimeからInstantを取り出すメソッド

LocalDateTime#toInstant(ZoneOffset)です。

一方、InstantからDateに変換するメソッドDate#from(Instant)です。

これによって、LocalDateTimeからDateに変換できます。

出力は次のとおりです。

LocalDateTime [2014/10/24 14:09:20]
Date          [2014/10/24 14:09:20]

JPALocalDateTime

最後にJPALocalDateTimeを永続化する場合の方法です。

JPALocalDateTimeを含んだエンティティを保存する場合、

次のようにします。

  1. javax.persistence.AttributeConverter<LocalDateTime, java.sql.Timestamp>を実装したクラスを作成する
  2. 1.で実装したクラスに@Converterアノテーションを付与する
  3. @ConverterアノテーションautoApplytrueを設定する
  4. エンティティクラスのLocalDateTime型のフィールドに、@Convertアノテーションを付与する
  5. @Convertconverterに1.で実装したクラスを指定します
  6. persistence.xmlclassエレメントに1.で作成したクラスを追加する

1.のコンバーターは次のような感じです。

@Converter(autoApply = true)
public class LocalDateTimeToTimestampConverter implements AttributeConverter<LocalDateTime, Timestamp> {
    //データベースに保存するときに使われる(LocalDateTime→Timestamp)
    @Override
    public Timestamp convertToDatabaseColumn(LocalDateTime localDateTime) {
        return Timestamp.valueOf(localDateTime);
    }
    //データベースから復元するときに使われる(Timestamp→LocalDateTime)
    @Override
    public LocalDateTime convertToEntityAttribute(Timestamp timestamp) {
        return timestamp.toLocalDateTime();
    }
}

3.のエンティティクラスは次のような感じです。

@Entity
@Table(name = "app_user")
@NamedQuery(name = "find-user-by-user-id", query = "select u from User u where u.userId = :userId")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "user_id", nullable = false, length = 30)
    private String userId;

    //作成したコンバーターを@Convertで指定する
    @Column(nullable = false)
    @Convert(converter = LocalDateTimeToTimestampConverter.class)
    private LocalDateTime created;

    //作成したコンバーターを@Convertで指定する
    @Column(nullable = false)
    @Convert(converter = LocalDateTimeToTimestampConverter.class)
    private LocalDateTime updated;

    public User() {}

    public User(String userId, LocalDateTime created) {
        this.userId = userId;
        this.created = created;
        this.updated = created;
    }
    //getter、setterを省略
    //toString、equals、hashCodeを省略
}

persistence.xmlの記述は次のような感じになります。

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">

    <persistence-unit name="test" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <!-- エンティティクラス -->
        <class>org.mikeneck.time.jpa.User</class>
        <!-- コンバーター -->
        <class>org.mikeneck.time.LocalDateTimeToTimestampConverter</class>

        <exclude-unlisted-classes>true</exclude-unlisted-classes>

        <properties>
            <property name="hibernate.connection.url" value="jdbc:derby:db/test"/>
            <property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.EmbeddedDriver"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.DerbyTenSixDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.connection.username" value=""/>
            <property name="hibernate.connection.password" value=""/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

さて、これで簡単なテストを作って、流してみると、

次のようなログが出力されます。

INFO: HHH000227: Running hbm2ddl schema export
Hibernate: 
    create table app_user (
        id bigint generated by default as identity,
        created timestamp not null,
        updated timestamp not null,
        user_id varchar(30) not null,
        primary key (id)
    )

DDLtimestamp型で形成されている様子がわかります。


というわけで、JPAでもLocalDateTimeのフィールドを保存できそうです。

また、既存のエンティティに関してもLocalDateTime

移行させることは可能そうです。

また、参考にしたのが下記のページですが、

java.sql.Dateに対してはLocalDate型と変換するようです。

Using the Java 8 DateTime Classes with JPA!

なお、ZonedDateTimeOffsetDateTimeのようなゾーンつきの

日時については、JDBCにそもそもそのような概念がないらしいので、

文字列で保存するようです(´・ω・`)

まあ、文字列からZonedDateTimeを生成するのは、

お手のものですけどね。


以上