Před časem jsme začali systém přizpůsobovat novému trhu, který vyžaduje podporu časových pásem. Prvotní výzkum byl popsán v předchozím článku. Nyní se tento přístup mírně vyvinul pod vlivem reality. Tento článek popisuje problémy, které se vyskytly během diskusí, a konečné rozhodnutí, které je implementováno.
TL;DR
- Je nutné rozlišovat pojmy:
- UTC je místní čas v pásmu +00:00 bez efektu letního času
- DateTimeOffset – posun místního času od UTC ± NN:NN, kde posun je základní posun od UTC bez efektu DST (v C# TimeZoneInfo.BaseUtcOffset)
- DateTime – místní čas bez informací o časovém pásmu (atribut Kind ignorujeme)
- Rozdělte použití na externí a interní:
- Vstupní a výstupní data prostřednictvím rozhraní API, zprávy, exporty/importy souborů musí být přísně v UTC (typ DateTime)
- Uvnitř systému jsou data uložena spolu s offsetem (typ DateTimeOffset)
- Rozdělte použití ve starém kódu na kódy jiné než DB (C#, JS) a DB:
- Kód jiný než DB funguje pouze s místními hodnotami (typ DateTime)
- Databáze pracuje s lokálními hodnotami + offset (typ DateTimeOffset)
- Nové projekty (komponenty) používají DateTimeOffset.
- V databázi se typ DateTime jednoduše změní na DateTimeOffset:
- V typech polí tabulky
- V parametrech uložených procedur
- Nekompatibilní konstrukce jsou v kódu opraveny
- Informace o posunu je připojena k přijaté hodnotě (jednoduché zřetězení)
- Před návratem k jinému kódu než DB je hodnota převedena na místní
- Žádné změny v kódu mimo DB
- DST se řeší pomocí uložených procedur CLR (pro SQL Server 2016 můžete použít AT TIME ZONE).
Nyní podrobněji o obtížích, které byly překonány.
„Zakořeněné“ standardy IT průmyslu
Trvalo poměrně hodně času, než se lidé zbavili strachu z ukládání dat v místním čase s posunem. Když se před časem zeptáte zkušeného programátora:"Jak podporovat časová pásma?" – jediná možnost byla:„Použít UTC a převést na místní čas těsně před ukázkou“. Pod pokličkou implementace se skrýval fakt, že pro normální workflow stále potřebujete další informace, jako je offset a názvy časových pásem. S příchodem DateTimeOffset se takové podrobnosti objevily, ale setrvačnost „zkušenosti s programováním“ neumožňuje rychle souhlasit s dalším faktem:„Uložení místního data se základním offsetem UTC“ je stejné jako ukládání UTC. Další výhoda použití DateTimeOffset všude vám umožňuje delegovat kontrolu nad dodržováním časových pásem .NET Framework a SQL Server, přičemž lidské kontrole ponechává pouze okamžiky vstupu a výstupu dat ze systému. Lidské ovládání je kód napsaný programátorem pro práci s hodnotami data/času.
Abych tento strach překonal, musel jsem uspořádat více než jedno sezení s vysvětlením, prezentací příkladů a důkazem konceptu. Čím jednodušší a bližší příklady těm úlohám, které jsou v projektu řešeny, tím lépe. Začnete-li v diskusi „obecně“, vede to ke komplikaci porozumění a ztrátě času. Stručně:méně teorie – více praxe. Argumenty pro UTC a proti DateTimeOffset lze vztáhnout ke dvěma kategoriím:
- Standardní je „UTC po celou dobu“ a zbytek nefunguje
- UTC řeší problém s DST
Je třeba poznamenat, že UTC ani DateTimeOffset neřeší problém s DST bez použití informací o pravidlech pro převod mezi zónami, které jsou dostupné prostřednictvím třídy TimeZoneInfo v C#.
Zjednodušený model
Jak jsem poznamenal výše, ve starém kódu se změny dějí pouze v databázi. To lze posoudit pomocí jednoduchého příkladu.
Příklad modelu v 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'
Výsledek spuštění skriptu bude následující.
Příklad ukazuje, že tento model umožňuje provádět změny pouze v databázi, což výrazně snižuje riziko defektů.
Příklady funkcí pro zpracování hodnot data/času
// 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
Malé artefakty
Během úpravy kódu SQL byly nalezeny některé věci, které fungují pro DateTime, ale nejsou kompatibilní s DateTimeOffset:
GETDATE()+1 musí být nahrazeno DATEADD (den, 1, SYSDATETIMEOFFSET ())
Klíčové slovo DEFAULT není kompatibilní s DateTimeOffset, musíte použít SYSDATETIMEOFFSET()
Konstrukce ISNULL(date_field, NULL)> 0″ funguje s DateTime, ale DateTimeOffset by měl být nahrazen „date_field IS NOT NULL“
Závěr nebo UTC vs. DateTimeOffset
Někdo si může všimnout, že stejně jako v přístupu s UTC se zabýváme konverzí při příjmu a vracení dat. Tak proč to všechno potřebujeme, když existuje osvědčené a fungující řešení? Existuje pro to několik důvodů:
- DateTimeOffset vám umožňuje zapomenout, kde se SQL Server nachází.
- To vám umožní přesunout část práce do systému.
- Převod lze minimalizovat, pokud se všude používá DateTimeOffset, a to pouze před zobrazením dat nebo jejich odesláním do externích systémů.
Tyto důvody se mi zdály zásadní vzhledem k použití tohoto přístupu.
Rád odpovím na vaše dotazy, pište komentáře.