web-dev-qa-db-ja.com

ZoneOffsetでOffsetDateTimeを保存するJPA

次のエンティティクラスがあります。

@Entity
public class Event {
    private OffsetDateTime startDateTime;
    // ...
}

ただし、JPA 2.2を使用してデータベースとの間でエンティティを永続化してから読み取ると、情報が失われますZoneOffsetstartDateTimeUTC(データベースのタイムスタンプで使用されるZoneOffset)。例えば:

Event e = new Event();
e.setStartDateTime(OffsetDateTime.parse("2018-01-02T09:00-05:00"));

e.getStartDateTime().getHour(); // 9
e.getStartDateTime().getOffset(); // ZoneOffset.of("-05:00")
// ...
entityManager.persist(e); // Stored startDateTime as timestamp 2018-01-02T14:00Z

Event other = entityManager.find(Event.class, e.getId());
other.getStartDateTime().getHour(); // 14 - DIFFERENT
other.getStartDateTime().getOffset(); // ZoneOffset.of("+00:00") - DIFFERENT

OffsetDateTimeを使用する必要があります。ゾーンルールが変更されるため、ZonedDateTimeは使用できません(とにかくこの情報の損失も発生します)。 LocalDateTimeは世界中のどこででも発生するため、Eventを使用することはできません。精度上の理由から、元のZoneOffsetが必要です。ユーザーがイベントの開始時刻を入力するため、Instantを使用できません(イベントは予定のようなものです)。

要件:

  • JPA-QLのタイムスタンプで>, <, >=, <=, ==, !=比較を実行できる必要がある

  • 永続化されたZoneOffsetと同じOffsetDateTimeを取得できる必要がある

17

//編集:JPAバージョン2.1と2.2の違いを反映するように回答を更新しました。

//編集2:JPA 2.2仕様のリンクを追加


JPA 2.1の問題

JPA v2.1はJava 8タイプを認識せず、提供された値を文字列化しようとします。LocalDateTime、InstantおよびOffsetDateTimeの場合、toString()メソッドを使用し、対応する文字列をターゲットフィールド。

つまり、Java.sql.DateJava.sql.Timestampなど、対応するデータベースタイプに値を変換する方法をJPAに伝える必要があります。

AttributeConverterインターフェイスを実装および登録して、これを機能させます。

見る:

Adam Bienの誤った実装に注意してください。最初にLocalDateをゾーニングする必要があります。

JPA 2.2の使用

属性コンバータを作成しないでください。それらはすでに含まれています。

//アップデート2:

これは次の仕様で確認できます: JPA 2.2 spec 。最後のページまでスクロールして、時間タイプが含まれていることを確認します。

Jpql式を使用する場合は、必ずInstantオブジェクトを使用し、PDOクラスでもInstantを使用してください。

例えば.

// query does not make any sense, probably.
query.setParameter("createdOnBefore", Instant.now());

これはうまく機能します。

他の形式の代わりにJava.time.Instantを使用する

とにかく、たとえZonedDateTimeまたはOffsetDateTimeがあったとしても、データベースはタイムゾーンに関係なく瞬間を保存するため、データベースから読み取られる結果は常にUTCです。 タイムゾーンは、実際には情報(メタデータ)を表示するだけです

したがって、代わりにInstantを使用し、必要な場合にのみZonedまたはOffset Timeクラスに変換することをお勧めします。特定のゾーンまたはオフセットで時刻を復元するには、ゾーンまたはオフセットを独自のデータベースフィールドに個別に保存します。

JPQLの比較は、このソリューションで機能します。常にインスタントを使用し続けるだけです。

PS:最近、私はいくつかのSpringの人たちと話をしましたが、彼らはあなたがInstant以外のものを永続化しないことにも同意しました。インスタントだけが特定の時点であり、メタデータを使用して変換できます。

複合値を使用する

仕様によると JPA 2.2仕様 、CompositeValuesは言及されていません。つまり、仕様に組み込まれておらず、現時点では単一のフィールドを複数のデータベース列に永続化することはできません。 「複合」を検索し、IDに関連する言及のみを表示します。

ただし、この回答のこのコメントで述べられているように、Hibernateはこれを実行できる可能性があります。

実装例

この原則を念頭に置いてこの例を作成しました。拡張のために開き、変更のために閉じます。この原則の詳細については、こちらをご覧ください。 Wikipediaのオープン/クローズド原則

これは、現在のフィールドをデータベース(タイムスタンプ)に保持でき、追加の列を追加するだけで、害を与えないことを意味します。

また、エンティティはOffsetDateTimeのセッターとゲッターを保持できます。内部構造は、呼び出し元の問題ではありません。これは、この提案がAPIをまったく傷つけないことを意味します。

実装は次のようになります。

@Entity
public class UserPdo {

  @Column(name = "created_on")
  private Instant createdOn;

  @Column(name = "display_offset")
  private int offset;

  public void setCreatedOn(final Instant newInstant) {
    this.createdOn = newInstant;
    this.offset = 0;
  }

  public void setCreatedOn(final OffsetDateTime dt) {
    this.createdOn = dt.toInstant();
    this.offset = dt.getOffset().getTotalSeconds();
  }

  // derived display value
  public OffsetDateTime getCreatedOnOnOffset() {
    ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(this.offset);
    return this.createdOn.atOffset(zoneOffset);
  }
}
10
Ben