web-dev-qa-db-fra.com

LocalDateTime à ZonedDateTime

J'ai Java 8 application web Spring qui prendra en charge plusieurs régions. J'ai besoin de créer des événements de calendrier pour un emplacement client. Disons donc que mon serveur Web et Postgres est hébergé dans le fuseau horaire MST (mais je suppose cela peut être n'importe où si nous passons au cloud.) Mais le client est en EST. Donc, en suivant certaines des meilleures pratiques que j'ai lues, je pensais que je stockerais toutes les heures de la date au format UTC. Tous les champs de date et d'heure de la base de données sont déclarés comme TIMESTAMP.

Voici donc comment prendre un LocalDateTime et le convertir en UTC:

ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(dto.getStartDateTime(), ZoneOffset.UTC, null);
//appointment startDateTime is a LocalDateTime
appointment.setStartDateTime( startZonedDT.toLocalDateTime() );

Maintenant, par exemple, lorsqu'une recherche de date arrive, je dois convertir la date et l'heure demandées en UTC, obtenir les résultats et convertir le fuseau horaire de l'utilisateur (stocké dans la base de données):

ZoneId  userTimeZone = ZoneId.of(timeZone.getID());
ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(appointment.getStartDateTime(), userTimeZone, null);
dto.setStartDateTime( startZonedDT.toLocalDateTime() );

Maintenant, je ne suis pas sûr que ce soit la bonne approche. Je me demande également si, parce que je passe de LocalDateTime à ZonedDateTime et vice versa, je risque de perdre des informations de fuseau horaire.

Voici ce que je vois et cela ne me semble pas correct. Lorsque je reçois le LocalDateTime de l'interface utilisateur, j'obtiens ceci:

2016-04-04T08:00

The ZonedDateTime =

dateTime=2016-04-04T08:00
offset="Z"
zone="Z"

Et puis quand j'attribue cette valeur convertie à mon rendez-vous LocalDateTime je stocke:

2016-04-04T08:00

J'ai l'impression que parce que je stocke dans LocalDateTime, je perds le fuseau horaire que j'ai converti en ZonedDateTime.

Dois-je faire en sorte que mon entité (rendez-vous) utilise ZonedDateTime au lieu de LocalDateTime pour que Postgres ne perde pas ces informations?

---------------- Modifier --------------- -

Après l'excellente réponse de Basils, j'ai réalisé que j'avais le luxe de ne pas me soucier du fuseau horaire des utilisateurs - tous les rendez-vous se rapportent à un emplacement spécifique afin que je puisse stocker toutes les heures en UTC, puis les convertir en fuseau horaire lors de la récupération. J'ai fait le suivi suivant question

25
sonoerin

Postgres n'a pas de type de données tel que TIMESTAMP. Postgres a deux types pour la date plus l'heure: TIMESTAMP WITH TIME ZONE et TIMESTAMP WITHOUT TIME ZONE. Ces types ont un comportement très différent en ce qui concerne les informations de fuseau horaire.

  • Le type WITH utilise toutes les informations de décalage ou de fuseau horaire pour régler la date-heure sur UTC , puis supprime ce décalage ou ce fuseau horaire; Postgres n'enregistre jamais les informations de décalage/zone.
    • Ce type représente un moment, un point spécifique sur la chronologie.
  • Le type WITHOUT ignore les informations de décalage ou de zone éventuellement présentes.
    • Ce type pas représente un moment. Cela représente une vague idée de potentiel moments sur une plage d'environ 26-27 heures (la plage de fuseaux horaires à travers le monde).

Vous voulez pratiquement toujours le type WITH, comme expliqué ici par l'expert David E. Wheeler. Le WITHOUT n'a de sens que lorsque vous avez la vague idée d'une date-heure plutôt que d'un point fixe sur la chronologie. Par exemple, "Noël cette année commence le 2016-12-25T00: 00: 00" serait stocké dans le WITHOUT tel qu'il s'applique au fuseau horaire any, sans avoir encore été appliqué à un seul fuseau horaire pour obtenir un moment réel sur la chronologie. Si les elfes du Père Noël suivaient l'heure de début pour Eugene Oregon US, alors ils utiliseraient le type WITH et une entrée qui incluait un décalage ou un fuseau horaire tel que 2016-12-25T00:00:00-08:00 qui est enregistré dans Postgres en tant que 2016-12-25T08:00.00Z (où Z signifie zoulou ou UTC).

L'équivalent de Postgres ’TIMESTAMP WITHOUT TIME ZONE dans Java.time est Java.time.LocalDateTime. Comme votre intention était de travailler en UTC (une bonne chose), vous devriez pas utiliser LocalDateTime (une mauvaise chose). Cela peut être le principal point de confusion et de problèmes pour vous. Vous pensez toujours à utiliser LocalDateTime ou ZonedDateTime mais vous ne devriez utiliser ni l'un ni l'autre; à la place, vous devez utiliser Instant (décrit ci-dessous).

Je me demande également si, parce que je passe de LocalDateTime à ZonedDateTime et vice versa, je risque de perdre des informations de fuseau horaire.

En effet, vous l'êtes. Le point entier vers LocalDateTime est vers perdre info sur le fuseau horaire. Nous utilisons donc rarement cette classe dans la plupart des applications. Encore une fois, l'exemple de Noël. Ou un autre exemple, "Politique de l'entreprise: toutes nos usines du monde entier déjeunent à 12h30". Ce serait LocalTime, et pour une date particulière, LocalDateTime. Mais cela n'a aucune signification réelle, pas un point réel sur la timeline, jusqu'à ce que vous appliquiez un fuseau horaire pour obtenir un ZonedDateTime . Cette pause déjeuner aura lieu à des moments différents de la chronologie dans l'usine de Delhi que dans l'usine de Düsseldorf et à nouveau dans l'usine de Détroit.

Le mot "local" dans LocalDateTime peut être contre-intuitif car il signifie pas de particulier localité. Lorsque vous lisez "Local" dans un nom de classe, pensez "Pas un instant… pas sur la chronologie… juste une idée floue sur une date-heure un peu-sorta".

Vos serveurs doivent presque toujours être définis sur UTC dans le fuseau horaire de leur système d'exploitation. Mais votre programmation ne devrait jamais dépendre de cette externalité car il est trop facile pour un administrateur système de la changer ou pour toute autre application Java pour changer le fuseau horaire par défaut actuel au sein de la JVM. Spécifiez donc toujours votre fuseau horaire souhaité/prévu (il en va de même pour Locale, soit dit en passant).

Résultat:

  • Vous travaillez trop dur.
  • Les programmeurs/administrateurs système doivent apprendre à "penser global, présenter local".

Pendant la journée de travail tout en portant votre casque geek, pensez à UTC. Ce n’est qu’à la fin de la journée, lors du passage à votre laïc, que vous devriez revenir à l’heure locale de votre ville.

Votre logique métier doit se concentrer sur l'UTC. Le stockage de votre base de données, la logique métier, l'échange de données, la sérialisation, la journalisation et votre propre réflexion doivent tous être effectués dans le fuseau horaire UTC (et 24 heures, au fait). Lorsque vous présentez des données aux utilisateurs, n'appliquez ensuite qu'un fuseau horaire particulier. Considérez les dates-heures zonées comme une chose externe, et non comme une partie fonctionnelle des internes de votre application.

Du côté Java côté, utilisez Java.time.Instant (un instant sur la chronologie en UTC) dans une grande partie de votre logique métier.

Instant now = Instant.now();

Espérons que les pilotes JDBC seront éventuellement mis à jour pour traiter directement les types Java.time comme Instant. Jusque-là, nous devons utiliser les types Java.sql. L'ancienne classe Java.sql a de nouvelles méthodes de conversion de/vers Java.time.

Java.sql.TimeStamp ts = Java.sql.TimeStamp.valueOf( instant );

Maintenant, passez ça Java.sql.TimeStamp objet via setTimestamp sur un PreparedStatement à enregistrer dans une colonne définie comme TIMESTAMP WITH TIME ZONE dans Postgres.

Pour aller dans l'autre sens:

Instant instant = ts.toInstant();

C'est donc facile, de passer de Instant à Java.sql.Timestamp à TIMESTAMP WITH TIME ZONE, tous en UTC. Aucun fuseau horaire impliqué. Le fuseau horaire par défaut actuel de votre système d'exploitation serveur, de votre machine virtuelle Java et de vos clients n'est pas pertinent.

Pour présenter à l'utilisateur, appliquez un fuseau horaire. Utilisez noms de fuseau horaire appropriés , jamais les codes à 3-4 lettres tels que EST ou IST.

ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

Vous pouvez vous ajuster dans une zone différente selon vos besoins.

ZonedDateTime zdtKolkata = zdt.withZoneSameInstant( ZoneId.of( "Asia/Kolkata" ) );

Pour revenir à un Instant, un instant sur la timeline en UTC, vous pouvez extraire du ZonedDateTime.

Instant instant = zdt.toInstant();

Aucun endroit où nous avons utilisé LocalDateTime .

Si vous obtenez un élément de données sans décalage par rapport à UTC ou fuseau horaire, tel que 2016-04-04T08:00, ces données vous sont entièrement inutiles (en supposant que nous ne parlons pas des scénarios de type Noël ou Déjeuner d'entreprise discutés ci-dessus). Une date-heure sans décalage/info de zone est comme un montant monétaire sans indication de devise: 142.70 ou même $142.70 -- inutile. Mais USD 142.70, ou CAD 142.70, ou MXN 142.70… Ceux-ci sont utiles.

Si vous obtenez cela 2016-04-04T08:00 valeur, et vous êtes absolument certain du contexte offset/zone prévu, alors:

  1. Analysez cette chaîne en tant que LocalDateTime.
  2. Appliquez un décalage par rapport à UTC pour obtenir un OffsetDateTime, ou (mieux) appliquez un fuseau horaire pour obtenir un ZonedDateTime.

Comme ce code.

LocalDateTime ldt = LocalDateTime.parse( "2016-04-04T08:00" );
ZoneId zoneId = ZoneId.of( "Asia/Kolkata" ); // Or "America/Montreal" etc.
ZonedDateTime zdt = ldt.atZone( zoneId ); // Or atOffset( myZoneOffset ) if only an offset is known rather than a full time zone.

Votre question est vraiment un double de beaucoup d'autres. Ces questions ont été discutées à plusieurs reprises dans d'autres questions et réponses. Je vous invite à rechercher et à étudier Stack Overflow pour en savoir plus sur ce sujet.

JDBC 4.2

Depuis JDBC 4.2, nous pouvons directement échanger des objets Java.time avec la base de données. Pas besoin d'utiliser jamais Java.sql.Timestamp encore une fois, ni ses classes apparentées.

Stockage, en utilisant OffsetDateTime comme défini dans la spécification JDBC.

myPreparedStatement.setObject( … , instant.atOffset( ZoneOffset.UTC ) ) ;  // The JDBC spec requires support for `OffsetDateTime`. 

… Ou utilisez éventuellement Instant directement, si votre pilote JDBC le prend en charge.

myPreparedStatement.setObject( … , instant ) ;  // Your JDBC driver may or may not support `Instant` directly, as it is not required by the JDBC spec. 

Récupération.

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;

À propos de Java.time

Le cadre Java.time est intégré à Java 8 et versions ultérieures. Ces classes supplantent l'ancien hérité) gênant classes date-heure telles que Java.util.Date , Calendar , & SimpleDateFormat .

Le projet Joda-Time , maintenant en mode maintenance , conseille la migration vers les classes Java.time .

Pour en savoir plus, consultez le Tutoriel Oracle . Et recherchez Stack Overflow pour de nombreux exemples et explications. La spécification est JSR 31 .

Vous pouvez échanger des objets Java.time directement avec votre base de données. Utilisez un pilote JDBC compatible avec JDBC 4.2 ou version ultérieure. Pas besoin de chaînes, pas besoin de Java.sql.* Des classes.

Où obtenir les classes Java.time?

Le projet ThreeTen-Extra étend Java.time avec des classes supplémentaires. Ce projet est un terrain d'essai pour de futurs ajouts possibles à Java.time. Vous pouvez trouver ici des classes utiles telles que Interval , YearWeek , YearQuarter , et plus .

66
Basil Bourque