web-dev-qa-db-fra.com

JPA Stockage OffsetDateTime avec ZoneOffset

J'ai la classe d'entité suivante:

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

Cependant, la persistance puis la lecture de l'entité vers/depuis la base de données avec JPA 2.2 entraîne une perte d'informations: le ZoneOffset de startDateTime se transforme en UTC (le ZoneOffset utilisé par l'horodatage de la base de données). Par exemple:

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

J'ai besoin d'utiliser OffsetDateTime: je ne peux pas utiliser ZonedDateTime, car les règles de zone changent (et cela souffre également de cette perte d'informations de toute façon). Je ne peux pas utiliser LocalDateTime, car un Event se produit n'importe où dans le monde et j'ai besoin du ZoneOffset d'origine à partir du moment où il s'est produit pour des raisons de précision. Je ne peux pas utiliser Instant car un utilisateur renseigne l'heure de début d'un événement (l'événement est comme un rendez-vous).

Exigences:

  • Besoin de pouvoir effectuer >, <, >=, <=, ==, != comparaisons d'horodatages dans JPA-QL

  • Besoin de pouvoir récupérer le même ZoneOffset que le OffsetDateTime qui a été conservé

17

// Edit: j'ai mis à jour la réponse pour refléter les différences entre JPA version 2.1 et 2.2.

// Edit 2: Ajout du lien de spécification JPA 2.2


Le problème avec JPA 2.1

JPA v2.1 ne connaît pas les types Java 8 et essaiera de filtrer la valeur fournie. Pour LocalDateTime, Instant et OffsetDateTime, il utilisera la méthode toString () et enregistrera la chaîne correspondante dans le champ cible.

Cela dit, vous devez indiquer à JPA comment convertir votre valeur en un type de base de données correspondant, comme Java.sql.Date ou Java.sql.Timestamp.

Implémentez et enregistrez l'interface AttributeConverter pour que cela fonctionne.

Voir:

Méfiez-vous de l'implémentation erronée d'Adam Bien: LocalDate doit d'abord être zoné.

Utilisation de JPA 2.2

Ne créez tout simplement pas les convertisseurs d'attributs. Ils sont déjà inclus.

// Mise à jour 2:

Vous pouvez le voir dans les spécifications ici: spécification JPA 2.2 . Faites défiler jusqu'à la toute dernière page pour voir que les types d'heure sont inclus.

Si vous utilisez des expressions jpql, assurez-vous d'utiliser l'objet Instant et également d'utiliser Instant dans vos classes PDO.

par exemple.

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

Cela fonctionne très bien.

En utilisant Java.time.Instant au lieu d'autres formats

Quoi qu'il en soit, même si vous avez un ZonedDateTime ou OffsetDateTime, le résultat lu dans la base de données sera toujours UTC, car la base de données stocke un instant dans le temps, quel que soit le fuseau horaire. Le fuseau horaire est en fait juste afficher des informations (métadonnées) .

Par conséquent, je recommande d'utiliser à la place Instant et de ne le convertir en classes de temps zoné ou décalé qu'en cas de besoin. Pour restaurer l'heure à la zone ou au décalage donné, stockez la zone ou le décalage séparément dans son propre champ de base de données.

Les comparaisons JPQL fonctionneront avec cette solution, continuez à travailler avec des instants tout le temps.

PS: J'ai récemment parlé à des gars de Spring, et ils ont également convenu que vous ne persistiez à rien d'autre qu'un instant. Seul un instant est un moment précis, qui peut ensuite être converti à l'aide de métadonnées.

Utilisation d'une valeur composite

Selon la spécification spécification JPA 2.2 , les valeurs composites ne sont pas mentionnées. Cela signifie qu'ils n'ont pas été inclus dans la spécification et que vous ne pouvez pas conserver un seul champ dans plusieurs colonnes de base de données pour le moment. Recherchez "Composite" et ne voyez que les mentions liées aux identifiants.

Cependant, Hibernate pourrait être capable de le faire, comme mentionné dans les commentaires de cette réponse.

Exemple d'implémentation

J'ai créé cet exemple avec ce principe à l'esprit: ouvert pour l'extension, fermé pour la modification. En savoir plus sur ce principe ici: principe ouvert/fermé sur Wikipedia .

Cela signifie que vous pouvez conserver vos champs actuels dans la base de données (horodatage) et vous n'avez qu'à ajouter une colonne supplémentaire, ce qui ne devrait pas nuire.

De plus, votre entité peut conserver les setters et les getters d'OffsetDateTime. La structure interne ne devrait pas préoccuper les appelants. Cela signifie que cette proposition ne devrait pas du tout nuire à votre API.

Une implémentation pourrait ressembler à ceci:

@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