Il y a quelque temps, nous avons commencé à adapter le système au nouveau marché qui nécessite la prise en charge des fuseaux horaires. La recherche initiale a été décrite dans l'article précédent. Maintenant, l'approche a légèrement évolué sous l'influence des réalités. Cet article décrit les problèmes rencontrés lors des discussions et la décision finale qui est mise en œuvre.
TL;DR
- Il faut distinguer les termes :
- UTC est l'heure locale dans la zone +00:00, sans l'effet DST
- DateTimeOffset :décalage horaire local par rapport à UTC ± NN:NN, où le décalage correspond au décalage de base par rapport à UTC sans l'effet DST (en C# TimeZoneInfo.BaseUtcOffset)
- DateTime - heure locale sans information sur le fuseau horaire (nous ignorons l'attribut Kind)
- Divisez l'utilisation en externe et interne :
- Les données d'entrée et de sortie via l'API, les messages, les exports/imports de fichiers doivent être strictement en UTC (type DateTime)
- Dans le système, les données sont stockées avec le décalage (type DateTimeOffset)
- Divisez l'utilisation dans l'ancien code en code non-DB (C#, JS) et DB :
- Le code non-DB fonctionne uniquement avec des valeurs locales (type DateTime)
- La base de données fonctionne avec des valeurs locales + offset (type DateTimeOffset)
- Les nouveaux projets (composants) utilisent DateTimeOffset.
- Dans une base de données, le type DateTime change simplement en DateTimeOffset :
- Dans les types de champs de tableau
- Dans les paramètres des procédures stockées
- Les constructions incompatibles sont corrigées dans le code
- Les informations de décalage sont attachées à une valeur reçue (concaténation simple)
- Avant de revenir au code non-DB, la valeur est convertie en local
- Aucune modification du code non-DB
- L'heure d'été est résolue à l'aide des procédures stockées CLR (pour SQL Server 2016, vous pouvez utiliser AT TIME ZONE).
Maintenant, plus en détail sur les difficultés qui ont été surmontées.
Normes "profondément enracinées" de l'industrie informatique
Il a fallu beaucoup de temps pour soulager les gens de la peur de stocker les dates à l'heure locale avec décalage. Il y a quelque temps, si vous demandiez à un programmeur expérimenté :"Comment prendre en charge les fuseaux horaires ?" – la seule option était :« Utiliser UTC et convertir en heure locale juste avant la démonstration ». Le fait que pour un flux de travail normal, vous ayez toujours besoin d'informations supplémentaires, telles que les noms de décalage et de fuseau horaire, était caché sous le capot de l'implémentation. Avec l'avènement de DateTimeOffset, de tels détails sont apparus, mais l'inertie de «l'expérience de programmation» ne permet pas de s'accorder rapidement avec un autre fait:«Stocker une date locale avec un décalage UTC de base» revient au même que stocker UTC. Un autre avantage de l'utilisation de DateTimeOffset partout vous permet de déléguer le contrôle sur le respect des fuseaux horaires .NET Framework et SQL Server, laissant au contrôle humain uniquement les moments d'entrée et de sortie des données du système. Le contrôle humain est le code écrit par un programmeur pour travailler avec des valeurs de date/heure.
Pour surmonter cette peur, j'ai dû organiser plus d'une session avec des explications, des présentations d'exemples et des preuves de concept. Plus les exemples sont simples et proches des tâches résolues dans le projet, mieux c'est. Si vous commencez la discussion « en général », cela entraîne une complication de la compréhension et une perte de temps. En bref :moins de théorie – plus de pratique. Les arguments pour UTC et contre DateTimeOffset peuvent être liés à deux catégories :
- "UTC tout le temps" est la norme et le reste ne fonctionne pas
- UTC résout le problème avec DST
Il convient de noter que ni UTC ni DateTimeOffset ne résolvent le problème avec DST sans utiliser les informations sur les règles de conversion entre les zones, qui sont disponibles via la classe TimeZoneInfo en C#.
Modèle simplifié
Comme je l'ai noté ci-dessus, dans l'ancien code, les modifications ne se produisent que dans une base de données. Cela peut être évalué à l'aide d'un exemple simple.
Exemple de modèle en 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'
Le résultat de l'exécution du script sera le suivant.
L'exemple montre que ce modèle permet d'apporter des modifications uniquement dans la base de données, ce qui réduit considérablement le risque de défauts.
Exemples de fonctions de traitement des valeurs de date/heure
// 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
Petits artefacts
Lors de l'ajustement du code SQL, certaines choses ont été trouvées qui fonctionnent pour DateTime, mais sont incompatibles avec DateTimeOffset :
GETDATE()+1 doit être remplacé par DATEADD (jour, 1, SYSDATETIMEOFFSET ())
Le mot clé DEFAULT est incompatible avec DateTimeOffset, vous devez utiliser SYSDATETIMEOFFSET()
La construction ISNULL(date_field, NULL)> 0″ fonctionne avec DateTime, mais DateTimeOffset doit être remplacée par "date_field IS NOT NULL"
Conclusion ou UTC vs DateTimeOffset
Quelqu'un peut remarquer que, comme dans l'approche avec UTC, nous nous occupons de la conversion lors de la réception et du retour des données. Alors pourquoi avons-nous besoin de tout cela, s'il existe une solution éprouvée et qui fonctionne ? Il y a plusieurs raisons à cela :
- DateTimeOffset vous permet d'oublier où se trouve SQL Server.
- Cela vous permet de transférer une partie du travail au système.
- La conversion peut être minimisée si DateTimeOffset est utilisé partout, en ne l'exécutant qu'avant d'afficher les données ou de les exporter vers des systèmes externes.
Ces raisons m'ont semblé essentielles en raison de l'utilisation de cette approche.
Je serai heureux de répondre à vos questions, s'il vous plaît écrivez des commentaires.