Zeitstempel sind total einfach!
Zeitstempel werden sehr oft sowohl unter Entwicklern als auch auf Management-Ebene als eher triviales Problem betrachtet – meist getreu dem Motto „Ich weiß doch, wie ein Datum und eine Uhrzeit aussehen, was soll man da groß definieren müssen“. Planung und Umsetzung von Projekten bauen dann auf dieser Annahme auf – von Entscheidungen zur Datenbank und deren Tabellen-Layout zum Datenmodell bis zur Darstellung in einem Client für Endnutzer. Natürlich auf globale Endnutzerschaft ausgerichtet, wir leben ja schließlich im 21. Jahrhundert.
Es ist leider kein Witz, dass auf dieser Ausgangslage diverse Projekte auch in den Produktivbetrieb gegangen sind – und meist ist das konzeptionelle Problem eine ganze Weile niemandem aufgefallen. Bis dann Anfragen von Endnutzern ankommen wie diese:
In meinem Client steht, ich hätte diese Nachricht in 5 Stunden verfasst…. ?!?!?
Nur: Dann ist es meist schon zu spät.
… oder auch nicht
Grundsätzlich ist schon die Annahme, Zeitstempel seien ein triviales Problem, falsch. Der Hintergrund zunehmend globaler Adressierung verschärft es zusätzlich.
Im Wesentlichen gibt es zwei Kategorien von Einflüssen, die den korrekten Einsatz von Zeitstempeln erschweren, aber meist nicht oder zu grob betrachtet und beachtet werden – die menschliche und die technische.
Kategorie: Mensch
Neben der grundsätzlichen „Das ist doch total einfach!“-Annahme gibt es eine sehr lange Liste von detaillierteren Annahmen, die oft gemacht oder ohne weitere Prüfung von externer Seite übernommen werden, und dennoch samt und sonders falsch sind. Artikel wie dieser sind eine gute Basis um einen Eindruck zu bekommen, wie weit man mit solche Annahmen von der Realität entfernt ist oder sein kann. Hier einige ausgewählte Beispiele:
- There are always 24 hours in a day.
Strenggenommen hat kein Tag 24 Stunden, sondern einige Sekunden weniger. Dies wird durch die bekannte Schalttage (29.2.) langfristig ausgeglichen. Kurzfristig gibt es aber bspw. auch Schaltsekunden, durch die die 24h nicht eingehalten werden. - Any 24-hour period will always begin and end in the same day (or week, or month).
Tages-/Wochen-/Monats-/Jahreswechsel ! - The machine that a program runs on will always be in the GMT time zone.
In der Regel laufen Systeme mit der Zeitzone ihres jeweiligen Netzwerks, und damit eher selten in GMT. - Well, surely there will never be a change to the time zone in which a program has to run in production.
Sommer-/Winterzeit (engl. DST) ! - The server clock and the client clock will use the same time zone.
Häufige Annahme, gerade wenn „erst nachträglich“ globalisiert werden soll. - The duration of one minute on the system clock would never be more than an hour.
Schon etwas weniger offensichtlich. Kann aber in virtualisierten Umgebungen grundsätzlich zum Problem werden, wenn nicht auf hinreichende Synchronisierung bei Downtimes geachtet wird.
Auch wenn sich einige dieser Beispiele schon auf den ersten Blick als falsch erkennen lassen: Es gibt genügend produktiv laufende Software, die auf solchen Annahmen basiert!
Kategorie: Technik
Von technischer Seite bringen vor allem Standards diverse Aspekte ein, die zwar deterministischer sind als die menschlichen, aber die Problematik nicht wirklich vereinfachen.
- Jede Menge Zeitzonen – mit teilweise ungeraden Offsets (siehe Australien):
- Sommer-/Winterzeit und deren Ausnahmen – werden nicht in jeder Zeitzone angewendet, und auch nicht von jedem Staat in einer Zeitzone.
- Schaltjahre (Stichwort 29.2.)
- Schaltsekunden (immer mal wieder)
- Anwendung diverser unterschiedlicher lokaler Darstellungsformate, oft unregelmäßig global verteilt
Der letzte Punkt hat aufgrund des Chaos, den er oft anrichtet, einen gesonderten Abschnitt verdient.
Formate – für jeden etwas
Bei der Zusammenstellung von weltweit eingesetzten Datumsformate bekommt man schnell den Eindruck, dass hier ein ziemliches unübersichtliches Chaos herrscht und manche Formate nur existieren, damit eine bestimmter Teil sagen kann „Wir haben unser eigenes Datumsformat! HA!“ (Ich mach‘ mir ein Format Widdewidde wie es mir gefällt ….).
- Unix-Zeitstempel – Die Darstellung der Zeit als vergangene Sekunden oder Millisekunden seit dem 1.1.1970 00:00:00 UTC+0 ist vor allem auf technischer Ebene sehr verbreitet, und die meisten Werkzeuge zur Handhabung von Zeitstempeln können auch damit umgehen. Einzig ob ein gegebener Wert sich nun auf Sekunden oder Millisekunden bezieht, ist nicht unbedingt eindeutig – man kann zwar Annahmen über die „Sinnhaftigkeit“ eines Datum machen, aber ob sie korrekt sind, lässt sich oft nicht zweifelsfrei überprüfen.
- ISO – Wie für so viele andere Dinge gibt es einen ISO-Standard für die Darstellung von Zeitstempeln, ISO 8601. Bekannte Darstellungen sind bspw. 2017-07-19T09:57:16+00:00 (das sogenannte Extended Format). Wobei dieser Standard mehr als nur ein Format definiert, die meisten Werkzeuge aber nur mit einem Bruchteil davon umgehen können (oder wollen). Das genannte Beispiel wäre auch ohne – und : als Trenner ein valides Datum nach Standard, wird aber nur selten von Werkzeugen akzeptiert – selbige folgen standardmäßig fast immer dem sogenanten Extended Format, d.h. YYYY-MM-DDThh:mm:ss(±hh:mm|Z), wobei z.T. auch Millisekunden erlaubt sind (Format ss.sss). Allerdings stellen diese Werkzeuge in der Regel auch Funktionen zur Erstellungen von Formatierern bereit, um mehr oder weniger beliebige Formate verarbeiten zu können – solange man den manuellen Aufwand nicht scheut.
- Diverse lokale Standards
- Mehr als 40 Datumsformate weltweit sind standardisiert
- Zahlreiche „gewohnheitsmäßige“ Formate ohne Standardisierung
- Gelten z.T. über mehrere Zeitzonen hinweg, oder es gelten mehrere Formate in derselben Zeitzone
- In diversen Ländern weltweit sind mehrere Formate gültig und/oder gängig
- Wem diese Varianten noch nicht ausreichen, kann natürlich noch auf Eigenbauten wie
/Date(-2208992400000+0100)/
zurückgreifen. Das genannte Beispiel stammt aus den JSON-Serialisierern von .Net (MS) und wurde auch einige Zeit als Standard verwendet – mittlerweile ist es zwar nur noch eine schaltbare Option, aber noch in einigen System verfügbar. Natürlich sind solche Darstellungen nicht MS-spezifisch. Auch andere Anbieter stricken sich gerne ihr eigenes Format, aus Gründen, die man allgemein kaum nachvollziehen kann – oft sogar intern nicht. - Ein etwas speziellerer Fall sind UUIDs der Version 1, bei denen Zeitstempel in einer UUID codiert werden, um sie im Gegensatz zum eigentlichen Zeitstempel immer eindeutig unterscheidbar zu machen. Praktisch begegnet man dieser Variante abseits bestimmter Datenbanken (bspw. Cassandra) eher selten.
<day timestamp="9/20/2013"> <...> <fiveteen timestamp="7:00 AM"/> <fiveteen timestamp="7:15 AM"/> <fiveteen timestamp="7:30 AM" min="0.000" max="0.000" mean="0.000"/> <fiveteen timestamp="7:45 AM" min="0.268" max="0.268" mean="0.268"/> <fiveteen timestamp="8:00 AM" min="0.625" max="0.625" mean="0.625"/> <fiveteen timestamp="8:15 AM" min="1.599" max="1.599" mean="1.599"/> <...> </day>
Das dargestellte Datumsformat mit „/“ als Trenner gibt es mindestens in den Varianten MM/TT/YYYY und TT/MM/YYYY. Während das im obigen Beispiel noch eindeutig ist, da es (in unserem Kalender) nicht mehr als 12 Monate gibt, wäre ein Datum wie 9/12/2013 nicht mehr selbsterklärend.
Ein etwas besseres, aber immer noch unvollständiges Beispiel:
<power> <period>300</period> <values count="4"> <value timestamp="2012-11-19T12:45:36" standard-deviation="20.21" skewness="30.32">10.10</value> <value timestamp="2012-11-19T12:50:36" standard-deviation="50.54" skewness="60.65">40.43</value> <value timestamp="2012-11-19T12:55:36" standard-deviation="80.87" skewness="90.98">70.76</value> <value timestamp="2012-11-19T13:00:36" standard-deviation="-20" skewness="0">10</value> </values> </power>
Hier fehlt die Zeitzonenangabe, auf die sich das Datum bezieht.
Die beiden Beispiele entstammen einem bereits im Produktiveinsatz befindlichen Projekt. Es hat mehrere Wochen in Anspruch genommen, jemanden zu finden, der eine Aussage treffen konnte, auf welche Zeitzone sich die Datumsangaben beziehen, und wo man diese Auslesen kann – um dann festzustellen, dass diese statisch sind (also DST nicht berücksichtigen) und auch keine unmittelbare Möglichkeit liefern, die ID der Zielzeitzone und somit auf dem Umweg das aktuelle Offset zu ermitteln.
Wie man die Welt retten kann … zumindest ein wenig
Die Liste der möglichen Probleme bei Umgang mit und der Verwendung von Zeitstempeln ist offensichtlich sehr umfassend – was natürlich die Frage aufwirft, ob und falls ja, wie man das ganze potentielle Chaos noch in weitestgehend geordnete Bahnen lenken kann.
Grundsätzlich – ja, solange man einen gewissen Satz an Rahmen-Bedingungen beachtet und erfüllt.
Grundsätzlich
Achtung: Einiges aus diesem Bereich mag äußerst trivial klingen – das heißt aber nicht, dass man es „nebenbei“ abhaken kann oder sollte. Es sei denn, man will Murphy’s Law herausfordern.
- Erkennen und akzeptieren, dass man nicht alle potentiellen Probleme lösen kann – Dieser erste Punkt ist zugleich auch der wichtigste. Nicht auf alle der genannten Problematiken hat man im Rahmen der Entwicklung Einfluss – vor allem zu verwendende externe Schnittstellen zählen zu dieser Kategorie. Hier kann man oft höchstens auf Überzeugungsarbeit setzen, die aber keinen Erfolg garantieren kann. Bei den Aspekten, auf die man Einfluss hat, sollte man hingegen – auch wenn das jetzt subtil klingen mag – unnachgiebig auf technisch saubere Lösungen hinarbeiten und auf deren Einsatz bestehen; einmal gemachte technische Schulden lassen sich im Nachhinein nicht mehr ohne weiteres Beseitigen.
- Externe Schnittstellen sind von ihren jeweiligen Betreibern sauber zu halten – Im direkten Anschluss an den ersten Teil des vorherigen Punkts. Bei externen Schnittstellen muss immer damit gerechnet werden, dass spezielle oder sehr spezifische Varianten – wie in den Beispielen im obigen Abschnitt gezeigt – eingesetzt werden. Als einbindender Entwickler kann man sowas (meist) nicht ändern und sollte entsprechend auch nur wenig Energie auf dahingehende Versuche verwenden. Vielmehr ist anzuraten, die jeweiligen Entwickler auf eine vollständige Dokumentation hin festzunageln, um an die notwendigen Informationen für die korrekte Weiterverarbeitung zu kommen. Das kann und sollte durchaus mit entsprechendem Nachdruck geschehen, denn auch hier gilt: Technische Schulden wird keine Seite schnell wieder los.
- Marke Eigenbau ist nicht unbedingt eine Qualitätsmerkmal – Bei einigen der o.g. Aspekte ist mit an Sicherheit grenzender Wahrscheinlichkeit schon die Idee aufgekommen, eine eigene Lösung zu implementieren.
In kurzen Worten: Macht das bloß nicht!
In etwas länger: Wer schon einmal ein wenig in den Implementierungen verbreiteter Bibliotheken zum Umgang mit Zeitstempeln gestöbert hat, wird gesehen haben, dass diese nicht gerade kurz sind und schon für vermeintlich einfach Aufgaben wie „Sekunden hinzufügen“ oder „Tage hinzufügen“ einen augenscheinlich überraschend hohen Aufwand betreiben. Hinzu kommen jahrelange Entwicklung und zahlreiche automatisierte und manuelle Tests, um die Funktionalität zu prüfen. Der Gedanke, für ein komplexes Problem eine vermeintlich einfache Lösung zu haben, ist zwar verlockend, führt aber meist in die völlig falsche Richtung – und eben solche Umsetzungen zeigen, dass die Lösungen eben nicht einfach sind. Es ist also anzuraten, hier auf Werkzeuge und Bibliotheken zu setzen, die „battle-tested“ sind, also schon eine Weile existieren und sich im Produktiveinsatz bewiesen haben. - Voraussetzungen auf Systemebene schaffen – Wenn eine Anwendung Zeitstempel erzeugen soll, greift sie dafür in der Regel auf Informationen aus dem umliegenden System zurück. Während man bei Clients (egal ob Web oder App) wenig bis gar keine Kontrolle über deren Zeitstempel-Generierung hat, kann man im Fall einer Server-Anwendung sehr viel kontrollieren und bearbeiten. Voraussetzung dafür ist allerdings, dass das umgebende System eine korrekte Zeit liefert – denn egal ob nun native Anwendung oder eine mit Runtime (z.B. auf Basis von Go) oder eine mit VM (bspw. JVM oder BEAM), alle greifen auf die Zeitangabe des umgebenden Systems zurück. D.h. es sollte alles dafür getan werden, einen Fehler der lokalen Zeitangabe des Systems zu vermeiden oder zumindest schnell und automatisiert korrigieren zu können. Protokolle wie NTP zusammen mit vertrauenswürdigen externen Quellen wirken hier Wunder.
- Keine Informationen verwerfen, die nochmal wichtig sein könnten – Bei Zeitstempeln – wie bei vielen anderen Punkten – gilt: Was da ist, kann man immer noch wegwerfen. Aber was weg ist, ist weg. Darstellungen wie in den beiden XML-Ausschnitten weiter oben gezeigt sind das Resultat von Entscheidungen aus der Design-Phase, bei denen man zum Ergebnis gekommen ist, dass die Zeitzone eine für den Leser nicht mehr relevante Information ist – und dabei ohne Weiteres ignoriert, dass Datenaustauschformate und Darstellungen für Benutzer keinesfalls dasselbe sind.
Der Weg an dieser Stelle ist einfach: Keine Informationen verwerfen, von denen nicht absolut und ohne auch nur der Ansatz eines Zweifels festgestellt werden kann, dass man sie nie, nirgends und keinen Umständen mehr brauchen wird. Diese Formulierung kann und sollte auch gegenüber Auftraggebern an den Tag gelegt werden – spätestens bei ohne auch nur der Ansatz eines Zweifels bekommen die meisten kalte Füße und lenken doch ein. Und sollten sie es nicht tun, sind sie zumindest selbst Schuld, wenn aus dieser Entscheidung resultierend im Nachhinein etwas den Bach runtergeht. - Verwendung globaler statt lokaler Standards für Austauschformate – So relevant lokale Darstellungsstandards für die Präsentation gegenüber einem Endnutzer sind, so irrelevant und schlimmstenfalls irreführend und gefährlich sind sie für den Datenaustausch auf technischer Ebene. Für selbigen sollte grundsätzlich immer auf global bekannte und unterstützte Standards wie ISO-6801 (Extended Format) gesetzt werden, um ein möglichst hohes Maß an Kompatibilität und ein möglichst kleines Maß an potentiellen Missverständnissen gewährleisten zu können.
- Spezifische Formatierung und -Lokalisierung ist Aufgabe der Präsentation, nicht der Persistenz oder der API – Quasi als „follow-up“ des zuvor genannten Punkts. Lokale Formate für Zeitstempel werden von Endnutzern meist erwartet, da sie damit vertraut sind (Gewöhnungsprinzip). Ein Datum gemäß eines lokalen Formats zu formatieren und zu lokalisieren sollte aber ausschließlich die Aufgabe des jeweiligen Clients sein, denn ihm stehen in der Regel alle dafür notwendigen Locale-Informationen zur Verfügung, was man als Versender eines Datums nicht immer gewährleisten könnte, selbst wenn man es wollte. Wenn der Datenaustausch über allgemeine unterstützte Standards (s.o.) erfolgte, sollte es für selbigen kein Problem darstellen, dass Datum inhaltlich zu evaluieren und entsprechend der lokalen Gegebenheiten zu formatieren.
Kurzum: Datenaustausch -> globale Standards, Datenrepräsentation -> lokale Standards.
Technisch
Die grundsätzlichen Rahmenbedingungen haben auch Einfluss auf die technischen – wie genau, wird im Folgenden genauer betrachtet. Die Code-Beispiele stammen, soweit nicht anders angegeben, aus Scala unter Verwendung der Java-8 Time API.
Die Verwendung von Unix-(like-)Zeitstempeln bietet sich vor allem für interne Austauschformate an, also solche, die nicht in Antworten auf externe Anfragen verwendet werden bzw. dort inhaltlich keine Rolle spielen. Hintergrund ist die mangelnde Eindeutigkeit dieser Zeitstempeln – strenggenommen weiß man bei einem vorliegenden Wert nicht, ob es sich auf Sekunden oder Millisekunden bezieht, da beide Varianten gängig sind. Als Mensch lässt sich eine solche Unterscheidung ggf. noch auf Basis des jeweiligen Kontextes treffen – eine technische, automatisierte Entscheidung ist da schon aufwändiger und nicht allgemein zweifelsfrei möglich.
Wer eine Programmiersprache verwendet, die Value-Klassen oder Newtype-Definitionen unterstützen, sollte selbige für eine eindeutigere Definition des derartigen Zeitstempels verwenden – da der Wert sonst nur eine Zahl ist, ist er potentiell für versehentliche Verwendung in Berechnungen prädestiniert.
class UnixMsTimestamp(val time: Long) extends AnyVal {}
Bei Verwendung serialisierter Formate sollten möglichst vollständige und explizite Angaben verwendet werden, die im ISO-Format (siehe 8601) vorgegeben sind. Hierbei ist darauf zu achten, dass die meisten Werkzeuge nur das sogenannte Extended Format
YYYY-MM-DDThh:mm:ss.sss(Z|(+hh:mm)|(-hh:mm))
unterstützen:
val timestamp = OffsetDateTime.parse("2017-06-27T14:50:42.743+02:00")
Der Standard wiederum lässt auch Formate ohne Trennzeichen – bzw. : zu, da die genaue Anzahl an Ziffern pro Komponente festgelegt ist.
Zur Klarstellung sei hier gesagt, dass auch dieses Format keine unter Berücksichtigung aller Verwendungszwecke 100%-tige Korrektheit liefern kann – dies wäre nur auf Basis der Zonen-ID möglich, welche aber noch schwerer zu beschaffen ist als das Zeitzonen-Offset in Stunden und Minuten. In den meisten Fällen kann man mit dieser Einschränkung ohne weiteres arbeiten; einzige Ausnahme wäre die Abbildung bzw. Verschiebung eines Zeitstempels, wodurch sich wg. DST das Offset ändern kann.
val a11 = ZonedDateTime.parse("2017-02-27T14:50:42.743+01:00") val a1 = a11.withZoneSameInstant(ZoneId.of("Europe/Berlin")) val b1 = a11.toOffsetDateTime val c1 = a1.plusMonths(4) // java.time.ZonedDateTime = 2017-06-27T14:50:42.743+02:00[Europe/Berlin] val c2 = b1.plusMonths(4) // java.time.OffsetDateTime = 2017-06-27T14:50:42.743+01:00
Im obigen Beispiel sieht man, dass das Offset im Falle von a1 durch die zeitliche Verschiebung korrekt angepasst wird, bei b1 nicht. Im ersteren Falle ist dies nur möglich, da die ID der Zeitzone vorliegt und damit Zugriff auf Informationen zu Beginn und Ende der jeweiligen DST möglich ist, insb. auf die jeweiligen Ausnahmen.
An allen Stellen, bei denen im Nachhinein das genaue Zeitzonen-Offset bei Erstellung nicht mehr verwendet wird, sollte auf Zeitstempel mit UTC-0 als Zeitzone zurückgegriffen werden:
val timestamp = OffsetDateTime.parse("2017-06-27T14:50:42.743Z") val timestamp = OffsetDateTime.of(2017, 6, 27, 14, 50, 42, 743, ZoneOffset.UTC) val timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli(1498575042743L), ZoneOffset.UTC) val timestamp = OffsetDateTime.ofInstant(Instant.now(), ZoneOffset.UTC) val timestamp = OffsetDateTime.now(ZoneOffset.UTC)
Anmerkung: Der einzige wesentliche Unterschied zwischen OffsetDateTime und ZonedDateTime besteht in der Möglichkeit der Verwendung der Zeitzonen-ID – das letztere Format kann damit umgehen, dass erstere nicht. Wenn allerdings anstelle der ID nur ein Offset vorliegt, verhalten sich beide Formate äquivalent und können entsprechend auch ausgetauscht werden.
Für Berechnungen – bspw. das Hinzufügen oder Abziehen einer bestimmten Dauer – sollten immer die zugehörigen Utility-Funktionen der Bibliothek verwendet werden. Eigenbau mag für augenscheinlich einfache Modifikationen verlockend sein, sollte aber nicht überschätzt werden – die Utility-Funktionen gibt es letztlich nicht ohne Grund.
Eine konsistente Verwendung der o.g. Formate und der zugehörigen Utilities löst zwar das Problem potentiell falscher Zeitstempel nicht von allein, ist aber überaus wertvoll für das Verständnis durch die jeweiligen Entwickler, insbesondere hinsichtlich des jeweiligen Datenmodells. Auf diesem Weg kann man zumindest die potentiell auftretenden Probleme minimieren.
Generelle Empfehlung für Sprachen auf der JVM: Java 8 Time API mit OffsetDateTime oder ZonedDateTime für Zeitstempel verwenden.
Die Trickkiste
Da nun die Probleme auf Modell- und Austausch-Ebene gelöst sind, sollte einem praktischen Einsatz nichts weiter im Wege stehen… oder?
Leider doch. Während man Zeitstempel im Unix-Format trivial speichern kann – sind letztlich nur Zahlen – so ist es für Zeitstempel inklusive Zeitzonen-Offset mitunter deutlich schwieriger – zumindest wenn man sich nicht die Nutzung potentiell vorhandener Datums- bzw. Zeitoperationen verbauen will, was bei formatiertem Speichern der Fall wäre.
Es gibt hier mehrere Ansätze, die je nach DB-Typ und genauer Umsetzung sehr weit auseinander gehen können. MongoDB bspw. konvertiert einen Zeitstempel vor dem Speichern immer in UTC-0; wenn man das Offset mit Speichern will, muss man selbst tun (siehe hier). Für SQL-Datenbanken gibt es hierfür einen Standard-Typen:
TIMESTAMP WITH TIME ZONE
Dieser wird von den meisten SQL-Datenbanken unterstützt, auch wenn er mitunter anders benannt ist (bspw. DATETIMEOFFSET bei MS SQL Server). Intern wird dieser Zeitstempel ähnlich wie bei der genannten MongoDB gespeichert, allerdings transparent statt explizit.
Problematisch ist letztlich die Unterstützung des Standards in gängigen ORMs. Verbreitet wird oft nur ein kleines Subset der standardisierten Zeitstempel-Formate angeboten. Dies ist meist weniger ein Problem des ORMs selbst, sondern ein Resultat von Beschränkungen, die der darunter liegende Treiber auferlegt – oder der Unwille, diese Limitierungen zu umschiffen. Solche Beschränkungen tauchen vor allem auf, wenn ein Treiber mehrere Datenbanken unterstützen möchte – und sie werden meist umfassender, je breiter diese Unterstützung ist. Diese Einschränkungen gelten leider auch für JDBC – der o.g. Typ wird hier nicht direkt unterstützt und damit auch nicht von darauf aufsetzenden ORMs.
Was kann man also tun? Im obigen Abschnitt wurde erwähnt, dass man Limitierungen „umschiffen“ könnte, wenn man wollte. Das ist auch hier möglich, erfordert allerdings etwas „Trickserei“ und die Ausnutzung von Datenbank-Features zum setzen und lesen von Zeitstempeln. Die meisten erlauben nämlich, den Zeitstempel in formatierter Fassung zu schreiben und zu lesen. Es bietet sich also an, Zeitstempel mit Zeitzone gegenüber dem Treiber als Zeichenkette (String) zu behandeln und damit quasi „an JDBC vorbei“ zu schieben. Hierfür benötigt man lediglich Mapper für die beide zu berücksichtigenden Richtungen:
val msDateTimeOffset = new format.DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd HH:mm:ss") .appendFraction(ChronoField.MICRO_OF_SECOND, 3, 7, true) .appendLiteral(" ") .appendOffsetId() .toFormatter implicit val offsetDatetimeMapper = MappedColumnType.base[OffsetDateTime, String]( _.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), OffsetDateTime.parse(_, DataFormatter.msDateTimeOffset) )
Das genannte Beispiel stammt aus Slick, ist aber bspw. auch in jOOQ in ähnlicher Form möglich. Im Wesentlichen geht es um Festlegungen, dass ein dem Treiber standardmäßig unbekanntes Format wie ein anderes, bekanntes Format zu behandeln ist, und man diese unmittelbar vor dem Schreiben und nach dem Lesen noch abbilden möchte, bevor sie weitergereicht werden. Im obigen Fall ist dies ein Zeitstempel in Gestalt von OffsetDateTime, das Beispiel stammt aus einer Implementierung für MS SQL Server. Die Abbildung OffsetDateTime => String erfolgt über die Formatierung im ISO-8601 Extended Format (ist als Formatierer DateTimeFormatter.ISO_OFFSET_DATE_TIME in der Java 8 Time API verfügbar), der Rückweg über die parse-Funktion von OffsetDateTime mit einem entsprechend eigenen Formatierer, da die Abfrage leider keine ISO-formatierte Darstellung liefert. Der Rückweg muss entsprechend für die meisten Datenbanken eigens angepasst werden.
Web-Clients
Während man bei Anwendungen wie nicht-hybriden Smartphone-Apps in der Regel Zugriff auf APIs zur Verarbeitung von Zeitstempeln hat, die denen einer Server-Anwendung kaum nachstehen, so ist derselbe Vorgang bei Web-Clients ggf. ungleich schwieriger, je nachdem, was man genau umsetzen will – selbst wenn der Datentransport in Richtung Client auf standardisierten Formaten basiert.
Die native Date-API kann bspw. nur Zeitstempel mit der lokalen Zeitzone des Clients erstellen. Allerdings kann sie dies auch aus ISO-formatierten Zeitstempeln, projiziert diesen jedoch stets auf die lokale Zeitzone. Bibliotheken wie date-fns versprechen zwar sehr viel Utility im Umgang mit den so erstellten Date-Objekten, sind aber denselben Restriktionen unterworfen.
Die noch relativ junge Intl-API kann mit Intl.DateTimeFormat zwar Zeitstempel auch in andere Zeitzonen abbilden und weitestgehend beliebig formatieren, bietet aber selbst keinen Parser dafür an, weshalb man auf die Fähigkeiten des Date-Objekts zurückgreifen muss. In vergleichsweise vielen Fällen reicht diese Kombination allerdings völlig aus – nicht ohne Grund baut auch der Date-Filter von Angular darauf auf (bzw. basierte – in Version 5 wurde eine eigene Lösung integriert … siehe die obigen Abschnitte als Kommentar dazu).
Die umfassendste Lösung für die Bearbeitung von Zeitstempeln und deren Projektion in andere Darstellungen oder Zeitzonen bietet aktuell momentjs mit dem Zusatzmodul moment-timezone (alternativ bietet sich auch der noch recht junge Abkömmling Luxon an). Hier wird intern ein eigenes Darstellungsformat für die Zeitstempel verwendet, um die Restriktionen des nativen Date-Objekts zu umgesehen. Dieser Umfang an Möglichkeiten hat leider auch seinen Preis – das moment-timezone Modul enthält ein Informationspaket, um Zeitzonen über ihre ID zu verarbeiten und korrekt (auch hinsichtlich DST) abzubilden, welches minifiziert auf satte 174 KB kommt (~22KB mit gzip).
Letztlich kann man in etwa auf folgende Entscheidungsregeln zurückgreifen:
- Unterstützung für nicht ganz aktuelle Browser wird benötigt => moment.js (das Intl-Polyfill ist nicht gerade klein)
- Breite Unterstützung für Abbildungen in Zeitzonen wird benötigt => moment.js
- Sonst => Date-Objekte und Intl.DateTimeFormat; ggf. Teile von date-fns dazunehmen (ist modular, zusätzliche Größe sollte also überschaubar sein)
Ende
Der Umgang mit Zeitstempeln ist doch nicht so einfach, wie es oft – zu oft – angenommen wird. Mit einem ausgeprägtem Bewusstsein für das Problem, die Verwendung sinnvoller Setups, Werkzeuge und Bibliotheken sowie hin und wieder einem mehr-oder-weniger kräftigen verbalen Anschub in Richtung Design-Entscheider ist das Ganze aber durchaus behandelbar…
… solange man es auch entsprechend testet: Unit-Tests, Integration-Tests, und nicht zu vergessen: E2E-Tests!