Strefy czasowe: OffsetDateTime czy LocalDateTime? A może ZonedDateTime?

1

Mam w robocie typowego CRUDa w Springu (minuta ciszy dla mnie), w Javie 11. CRUD robi ekspansję za ocean, więc trzeba zaimplementować wybór strefy czasowej użytkownika i operować poprawnie na datach i czasach. Problemów z datami było już sporo przy jednej strefie czasowej, a operacje na nich to smutek rozpacz i cierpienie, bo wszystkie daty są trzymane w OffsetDateTime, a z frontendu przychodzi czas lokalny. Jest to ogarnięte jakimś dzikim voodoo w stylu "odejmij godzinę, dodaj 3 minuty, podskocz na lewej nodze a jak zrobisz to ładnie, to zadziała".

No i rozgorzała w teamie dyskusja, jak to powinno być zrobione, czy w encjach powinno być OffsetDateTime, czy nie, czy konwertować OffsetDateTime z encji na LocalDateTime, czy nie. Nagle trzeba użyć Javy, więc wiadomo, pod górkę. Ludzie mają jakieś tam opinie, ale średnio im ufam, bo widziałam już w ich wykonaniu return null;, operacje na pieniądzach na Double i 20 dependencies w jednym Service, a także inne trololo typowe dla programistów Springa, którzy lubią żyć na krawędzi.

Moje pytanie brzmi: Jak to powinno być zrobione, żeby było dobrze - tak by the book? Jaki typ w encjach? Jaki typ w service? Co zwracać w DTOsach frontowi? Mamy scheduler, który na przykład wysyła przypomnienia użytkownikom, jeśli tego dnia upływa termin zrobienia czegoś, jak to ogarnąć przy różnych strefach czasowych?

Mój koncept jest taki: w bazie wszystko w UTC, w service operujemy na LocalDateTime z UTC, a na koniec zwracamy frontowi LocalDateTime ze strefą użytkownika. Ale czy to wymaga, żeby migrować encje na LocalDateTime? Albo może wręcz przeciwnie, niech nas ręka boska broni?

0

A czemu z frontu przychodzi data z timezonem a nie timestamp?

0

@TomRiddle: Szczerze, to nie wiem, tak zastałam. Ale załóżmy, że przychodziłby timestamp, bo tak jest poprawniej. Przerzucamy timestamp na LocalDateTime z UTC, robimy jakieś operacje, checki, cokolwiek, zapisujemy do bazy. Pytanie wciąż istnieje: Jaki powinien być typ w encji zapisywanej do bazy? OffsetDateTime? LocalDateTime? Jaki powinien być typ kolumny w bazie? (MySQL)

Albo w drugą stronę: czytamy z bazy, robimy jakieś operacje w service, zwracamy timestamp frontendowi. Żeby te operacje móc łatwo wykonywać, zakładam że używalibyśmy LocalDateTime - w której warstwie konwertujemy tę datę? Już na etapie encji, czy może w service?

2
aliszja napisał(a):

Jaki powinien być typ w encji zapisywanej do bazy? OffsetDateTime? LocalDateTime? Jaki powinien być typ kolumny w bazie? (MySQL)

Moim zdaniem typ kolumny w MySql to powinien być TIMESTAMP.

Czytaj: https://dev.mysql.com/doc/refman/8.0/en/datetime.html

6

Użyj Instant. LocalDateTime nie ma informacji o strefie czasowej, OffsetDateTime ma stały offset bez uwzględnienia np. zmiany czasu. Najwiecej info posiada ZonedDateTime, natomiast zakładam, że Ciebie interesuje punkt w czasie.

0

Tak ogólnie biorąc to trochę zależy od tego co aplikacja robi. Jeśli chcesz np schedulować spotkania na kilka miesięcy w przód, i te spotkania mają być o 7 rano każdego dnia, to OffsetDateTime się nie nadaje bo zje ci zmianę czasu. LocalDateTime też niezbyt - możesz myśleć że sobie policzysz z góry te czasy spotkań na pół roku do przodu, a tutaj jakiś kraj przestanie uwzględniać zmianę czasu, i jesteś w dupie (vs podmianka tzdata). Dodatkowo wyobraź sobie że ktoś jest właścicielem standupów danego zespołu ale pojechał sobie na konferencję do stanów i przestawił preferencje strefy czasowej - czy teraz spotkanie powinno być o 10:30 rano czasu w Polsce, czy czasu tej osoby która założyła spotkanie?

W twoim przypadku trzeba się zastanowić - czy ważna w tym schedulerze jest konkretna lokalna godzina (np musisz wziąć urlop na żądanie przed 9:00) czy nie (masz 72 godziny na zasubmitowanie rozwiązania jakiegoś quizu)?

0

Po stronie BE to powinno być zapisane w jakimś jednolitym formacie, może być nawet czas z polskiej strefy - byleby było spójnie. Chociaż faktycznie strefa Z będzie do tego najlepsza. Do tego (jeśli potrzeba) strefa użytkownika powinna lecieć w headerze/w sesji/w bazie danych (zależnie od konfiguracji) jeśl zajdzie taka potrzeba.

Największy problem jest taki, żeby zapewnić, że front będzie się trzymał konwencji - tj. żeby przysyłał informacje na temat strefy w jakimś jednym formacie.

0

@Zing: Zdecydowanie konkretna godzina. Założenie jest takie: user wybiera w swoich ustawieniach, w jakiej strefie czasowej operuje. Potem, powiedzmy, że składa zamówienie i ustala datę i czas ważności tego zamówienia. Osoby, które na to zamówienie odpowiadają, mają poustawiane swoje strefy czasowe. Ważne jest, żeby pokazać im datę i czas upływu ważności zamówienia w ich strefie czasowej, i wysłać przypomnienie mailowo. Teraz przypomnienie przychodzi po prostu o 9:00 rano dnia, kiedy ten deadline upływa. Zakładam, że trzeba będzie podejść do tego bardziej na zasadzie "przypomnij 12h przed deadlinem", tylko co jeśli to będzie środek nocy, a klienci życzą sobie dostawać maile z przypomnieniami rano?

@wartek01: Wybrana strefa użytkownika będzie zapisana w bazie, bo użytkownicy chcą ją sobie ustawiać z panelu opcji użytkownika.

6

Tylko daty i tak bym przechowywał spójnie w UTC, a potem konwertował na potrzeby widoków.

2

@aliszja: Trzymaj czas w bazie w UTC i posługuj się nim wszędzie w aplikacji, i dopiero przy wyświetlaniu userowi daty wyświetl ją z wykorzystaniem strefy czasowej.

0

Dzięki za wszystkie Wasze podpowiedzi. Wnioski mam takie: Między LocalDateTime a OffsetDateTime jest znacząca różnica - taka, że OffsetDateTime reprezentuje punkt w czasie, a LocalDateTime po prostu reprezentuje datę. To był ważny kawałek informacji, którego mi było brak. Jeśli porównujemy ze sobą daty w różnych strefach czasowych, to tak naprawdę powinniśmy porównywać dwa punkty w czasie. Żeby stwierdzić, czy te punkty w czasie są zbieżne/większe/mniejsze, byłoby dobrze sprowadzić je do jednej strefy czasowej (np. UTC). ZonedDateTime byłby najbardziej "prawilny", bo ogarnia czas letni/zimowy i ma najwięcej informacji.

Większość problemów w tej aplikacji wynika z tego, że konwertując z LocalDateTime na OffsetDateTime i z powrotem, tracimy informację o tym, w jakim punkcie w czasie (momencie) się znajdujemy. Rozwiązaniem tego problemu byłoby upewnienie się, że w komunikacji z frontendem operujemy na momentach, a frontend interpretuje ten moment i wyświetla go użytkownikowi w jego strefie czasowej i w drugą stronę - frontend wysyła backendowi punkt w czasie.

Musimy też zadbać o to, żeby w bazie też zapisywać jakiś timestamp, a nie reprezentację daty.

0

Jeśli łączysz się do bazy za pomocą sterownika JDBC to proponuję do zajrzenia do specyfikacji JDBC 4.2 załącznik B.
Jest tam napisane, że występują następujące mapowania:

SQL Timestamp with timezone <-> JAVA OffsetDateTime
SQL Timestamp without timezone <-> JAVA LocalDateTime

Teraz musisz zobaczyć w dokumentacji sterownika JDBC do MySQL jak on te typy ze specyfikacji implementuje.

Używanie OffsetDateTime na backendzie oraz do komunikacji z backendem moim zdaniem jest najlepszym rozwiązaniem.

0
Charles_Ray napisał(a):

Tylko daty i tak bym przechowywał spójnie w UTC, a potem konwertował na potrzeby widoków.

TIMESTAMP w MySQL może trzymać dane zarówno ze strefą, jak i bez, co mu podasz to przyjmie (przynajmniej od wersji 8 - nie wiem jak w starszych). Żeby było śmieszniej, według docsów MySQL uczynnie przechowuje wszystkie timestampy w UTC, ale przy wyciąganiu może to skonwertować do innej strefy, zależnie od tego co siedzi w zmiennej systemowej, więc trzeba o tym pamiętać :D

Jeszcze pytanie, co się wydarzy przy mapowaniu do/z bazy w razie, gdyby ta strefa UTC nie była explicite zawarta w samym timestamp. Wydaje mi się, że kiedyś naciąłem się na takich konwersjach i efekt końcowy był taki, że do tego co wypluł MySQL serwer beztrosko doklejał swoją systemową strefę i do widzenia :D Więc ja bym asekuracyjnie, ze względu na brak zaufania do tego czego używam zrobił tak:

  • Trzymał w bazie timestampy w UTC, ale explicite narzucił informację że to jest UTC. W Postgres jest to o tyle proste, że masz osobny typ kolumny TIMESTAMP i TIMESTAMP WITH TIME ZONE, a w MySQL najwyraźniej stosuje się logikę rozmytą...
  • Zdefiniował ten sysvar dla MySQL na UTC, żeby nie przyszło mu do głowy uczynnie robić niechcianych konwersji
  • Po stronie serwera również pilnował, żeby za strefę czasową przyjmował UTC a nie systemową (albo zmienił systemową). Możesz wstrzykiwać sobie java.time.Clock tam, gdzie majstrujesz coś z czasem. Przy okazji, mogąc wstrzykiwać zamiast polegać na domyślnym po którego sięgnie sobie jakiś Instant.now() lub inny ancymon łatwiej będzie testować jakieś dziwne przypadki w stylu a co jak user ma inną strefę, serwer ma inną strefę, a baza ma inną?

1 użytkowników online, w tym zalogowanych: 0, gości: 1