Da tempo abbiamo iniziato ad adattare il sistema al nuovo mercato che richiede il supporto per i fusi orari. La ricerca iniziale è stata descritta nell'articolo precedente. Ora l'approccio si è leggermente evoluto sotto l'influenza della realtà. Questo articolo descrive i problemi incontrati durante le discussioni e la decisione finale adottata.
TL;DR
- È necessario distinguere i termini:
- UTC è l'ora locale nella zona +00:00, senza l'effetto DST
- DateTimeOffset – offset dell'ora locale da UTC ± NN:NN, dove l'offset è l'offset di base dall'UTC senza l'effetto DST (in C# TimeZoneInfo.BaseUtcOffset)
- DateTime – ora locale senza informazioni sul fuso orario (ignoriamo l'attributo Kind)
- Dividi l'uso in esterno e interno:
- I dati di input e output tramite API, messaggi, esportazioni/importazioni di file devono essere rigorosamente in formato UTC (tipo DateTime)
- All'interno del sistema, i dati vengono memorizzati insieme all'offset (tipo DateTimeOffset)
- Dividi l'uso nel vecchio codice in codice non DB (C#, JS) e DB:
- Il codice non DB funziona solo con valori locali (tipo DateTime)
- Il database funziona con valori locali + offset (tipo DateTimeOffset)
- I nuovi progetti (componenti) utilizzano DateTimeOffset.
- In un database, il tipo DateTime cambia semplicemente in DateTimeOffset:
- Tipi di campo nella tabella
- Nei parametri delle stored procedure
- Le costruzioni incompatibili sono state corrette nel codice
- Le informazioni di offset sono allegate a un valore ricevuto (concatenazione semplice)
- Prima di tornare al codice non DB, il valore viene convertito in locale
- Nessuna modifica al codice non DB
- L'ora legale viene risolta utilizzando le stored procedure CLR (per SQL Server 2016 è possibile utilizzare AT TIME ZONE).
Ora, più in dettaglio sulle difficoltà che sono state superate.
Standard "radicati" del settore IT
Ci è voluto molto tempo per alleviare le persone dalla paura di memorizzare le date nell'ora locale con offset. Qualche tempo fa, se chiedessi a un programmatore esperto:"Come supportare i fusi orari?" – l'unica opzione era:"Usa UTC e converti all'ora locale appena prima della dimostrazione". Il fatto che per il normale flusso di lavoro siano ancora necessarie informazioni aggiuntive, come i nomi di offset e fuso orario, è stato nascosto sotto il cofano dell'implementazione. Con l'avvento di DateTimeOffset sono emersi dettagli del genere, ma l'inerzia della "esperienza di programmazione" non consente di concordare rapidamente con un altro fatto:"Memorizzare una data locale con un offset UTC di base" equivale a memorizzare UTC. Un altro vantaggio dell'utilizzo di DateTimeOffset ovunque consente di delegare il controllo sull'osservanza dei fusi orari di .NET Framework e SQL Server, lasciando al controllo umano solo i momenti di input e output dei dati dal sistema. Il controllo umano è il codice scritto da un programmatore per lavorare con i valori di data/ora.
Per superare questa paura, ho dovuto tenere più di una sessione con spiegazioni, presentando esempi e Proof of Concept. Più semplici e vicini sono gli esempi a quei compiti che vengono risolti nel progetto, meglio è. Se inizi nella discussione "in generale", questo porta a una complicazione della comprensione e a una perdita di tempo. In breve:meno teoria – più pratica. Gli argomenti per UTC e contro DateTimeOffset possono essere correlati a due categorie:
- "UTC tutto il tempo" è lo standard e il resto non funziona
- UTC risolve il problema con l'ora legale
Va notato che né UTC né DateTimeOffset risolvono il problema con l'ora legale senza utilizzare le informazioni sulle regole per la conversione tra zone, disponibili tramite la classe TimeZoneInfo in C#.
Modello semplificato
Come ho notato sopra, nel vecchio codice, le modifiche avvengono solo in un database. Questo può essere valutato utilizzando un semplice esempio.
Esempio di modello in T-SQL
// 1) data storage // input data in the user's locale, as he sees them declare @input_user1 datetime = '2017-10-27 10:00:00' // there is information about the zone in the user configuration declare @timezoneOffset_user1 varchar(10) = '+03:00' declare @storedValue datetimeoffset // upon receiving values, attach the user’s offset set @storedValue = TODATETIMEOFFSET(@input_user1, @timezoneOffset_user1) // this value will be saved select @storedValue 'stored' // 2) display of information // a different time zone is specified in the second user’s configuration, declare @timezoneOffset_user2 varchar(10) = '-05:00' // before returning to the client code, values are reduced to local ones // this is how the data will look like in the database and on users’ displays select @storedValue 'stored value', CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user1)) 'user1 Moscow', CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user2)) 'user2 NY' // 3) now the second user saves the data declare @input_user2 datetime // input local values are received, as the user sees them in New York set @input_user2 = '2017-10-27 02:00:00.000' // link to the offset information set @storedValue = TODATETIMEOFFSET(@input_user2, @timezoneOffset_user2) select @storedValue 'stored' // 4) display of information select @storedValue 'stored value', CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user1)) 'user1 Moscow', CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user2)) 'user2 NY'
Il risultato dell'esecuzione dello script sarà il seguente.
L'esempio mostra che questo modello consente di apportare modifiche solo al database, il che riduce notevolmente il rischio di difetti.
Esempi di funzioni per l'elaborazione di valori di data/ora
// When receiving values from the non-DB code in DateTimeOffset, they will be local, // but with offset +00:00, so you must attach a user’s offset, but you cannot convert between // time zones. To do this, we translate the value into DateTime and then back with the indication of the offset // DateTime is converted to DateTimeOffset without problems, // so you do not need to change the call of the stored procedures in the client code create function fn_ConcatinateWithTimeOffset(@dto datetimeoffset, @userId int) returns DateTimeOffset as begin declare @user_time_zone varchar(10) set @user_time_zone = '-05:00' // from the user's settings @userId return todatetimeoffset(convert(datetime, @dto), @user_time_zone) end // Client code cannot read DateTimeOffset into variables of the DateTime type, // so you need to not only convert to a correct time zone but also reduce to DateTime, // otherwise, there will be an error create function fn_GetUserDateTime(@dto datetimeoffset, @userId int) returns DateTime as begin declare @user_time_zone varchar(10) set @user_time_zone = '-05:00' // from the user's settings @userId return convert(datetime, switchoffset(@dto, @user_time_zone)) end
Piccoli manufatti
Durante la modifica del codice SQL, sono state rilevate alcune cose che funzionano per DateTime, ma sono incompatibili con DateTimeOffset:
GETDATE()+1 deve essere sostituito con DATEADD (giorno, 1, SYSDATETIMEOFFSET ())
La parola chiave DEFAULT non è compatibile con DateTimeOffset, devi utilizzare SYSDATETIMEOFFSET()
Il costrutto ISNULL(date_field, NULL)> 0″ funziona con DateTime, ma DateTimeOffset dovrebbe essere sostituito con "date_field NON È NULL"
Conclusione o UTC vs DateTimeOffset
Qualcuno potrebbe notare che, come nell'approccio con UTC, ci occupiamo della conversione durante la ricezione e la restituzione dei dati. Allora perché abbiamo bisogno di tutto questo, se esiste una soluzione ben collaudata e funzionante? Ci sono diverse ragioni per questo:
- DateTimeOffset ti consente di dimenticare dove si trova SQL Server.
- Questo ti permette di trasferire parte del lavoro al sistema.
- La conversione può essere ridotta al minimo se DateTimeOffset viene utilizzato ovunque, eseguendolo solo prima di visualizzare i dati o inviarli a sistemi esterni.
Queste ragioni mi sono sembrate essenziali a causa dell'utilizzo di questo approccio.
Sarò felice di rispondere alle tue domande, per favore scrivi commenti.