Database
 sql >> база данни >  >> RDS >> Database

Повече за въвеждането на часови зони в дълготраен проект

Преди време започнахме да адаптираме системата към новия пазар, който изисква поддръжка за часови зони. Първоначалното изследване беше описано в предишната статия. Сега подходът леко еволюира под влиянието на реалностите. Тази статия описва проблемите, възникнали по време на дискусиите, и крайното решение, което се прилага.

TL;DR

  • Необходимо е да се разграничат термините:
    • UTC е местното време в зоната +00:00, без ефекта на DST
    • DateTimeOffset – отместване на местното време от UTC ± NN:NN, където отместването е основното изместване спрямо UTC без ефекта на DST (в C# TimeZoneInfo.BaseUtcOffset)
    • DateTime – местно време без информация за часовата зона (игнорираме атрибута Kind)
  • Разделете употребата на външна и вътрешна:
    • Входните и изходните данни чрез API, съобщения, експортиране/импортиране на файлове трябва да са строго в UTC (тип DateTime)
    • Вътре в системата данните се съхраняват заедно с отместването (тип DateTimeOffset)
  • Разделете употребата в стария код на не-DB код (C#, JS) и DB:
    • Не-DB код работи само с локални стойности (тип DateTime)
    • Базата данни работи с локални стойности + отместване (тип DateTimeOffset)
  • Новите проекти (компоненти) използват DateTimeOffset.
  • В база данни типът DateTime просто се променя на DateTimeOffset:
    • В типовете полета в таблицата
    • В параметрите на съхранените процедури
    • Несъвместимите конструкции са фиксирани в кода
    • Информацията за отместването е прикрепена към получена стойност (просто свързване)
    • Преди да се върнете към не-DB кода, стойността се преобразува в локална
  • Няма промени в не-DB код
  • DST се решава с помощта на CLR Съхранени процедури (за SQL Server 2016 можете да използвате AT TIME ZONE).

Сега по-подробно за преодолените трудности.

„Дълбоко вкоренени“ стандарти на ИТ индустрията

Отне доста време, за да се освободи хората от страха от съхраняване на дати в местно време с изместване. Преди време, ако попитате опитен програмист:„Как да поддържам часови зони?“ – единствената опция беше:„Използвайте UTC и преобразувайте в местно време точно преди демонстрацията“. Фактът, че за нормален работен процес все още се нуждаете от допълнителна информация, като имената на изместване и часови зони, беше скрит под капака на внедряването. С появата на DateTimeOffset се появиха подобни подробности, но инерцията на „изживяването при програмиране“ не позволява бързо да се съгласим с друг факт:„Съхраняването на локална дата с основно изместване на UTC“ е същото като съхраняването на UTC. Друго предимство на използването на DateTimeOffset навсякъде ви позволява да делегирате контрол върху спазването на часовите зони на .NET Framework и SQL Server, оставяйки за човешки контрол само моментите на въвеждане и извеждане на данни от системата. Човешкият контрол е кодът, написан от програмист, за да работи със стойности за дата/час.

За да преодолея този страх, трябваше да проведа повече от една сесия с обяснения, представяне на примери и Доказателство за концепция. Колкото по-прости и по-близки са примерите до тези задачи, които се решават в проекта, толкова по-добре. Ако започнете в дискусията „като цяло“, това води до усложняване на разбирането и загуба на време. Накратко:по-малко теория - повече практика. Аргументите за UTC и срещу DateTimeOffset могат да бъдат свързани с две категории:

  • „UTC през цялото време“ е стандартът и останалото не работи
  • UTC решава проблема с DST

Трябва да се отбележи, че нито UTC, нито DateTimeOffset решават проблема с DST без използване на информация за правилата за преобразуване между зони, която е достъпна чрез класа TimeZoneInfo в C#.

Опростен модел

Както отбелязах по-горе, в стария код промените се случват само в база данни. Това може да се оцени с прост пример.

Пример за модел в 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'

Резултатът от изпълнението на скрипта ще бъде както следва.

Примерът показва, че този модел позволява извършване на промени само в базата данни, което значително намалява риска от дефекти.

Примери за функции за обработка на стойности за дата/час

// 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

Малки артефакти

По време на настройката на SQL кода бяха открити някои неща, които работят за DateTime, но са несъвместими с DateTimeOffset:

GETDATE()+1 трябва да бъде заменен с DATEADD (ден, 1, SYSDATETIMEOFFSET ())

Ключовата дума DEFAULT е несъвместима с DateTimeOffset, трябва да използвате SYSDATETIMEOFFSET()

Конструкцията ISNULL(date_field, NULL)> 0″ работи с DateTime, но DateTimeOffset трябва да бъде заменен с „date_field IS NOT NULL“

Заключение или UTC срещу DateTimeOffset

Някой може да забележи, че както при подхода с UTC, ние се занимаваме с преобразуването при получаване и връщане на данни. Тогава защо ни трябва всичко това, ако има добре изпитано и работещо решение? Има няколко причини за това:

  • DateTimeOffset ви позволява да забравите къде се намира SQL Server.
  • Това ви позволява да прехвърлите част от работата към системата.
  • Преобразуването може да бъде сведено до минимум, ако DateTimeOffset се използва навсякъде, като се извършва само преди показване на данни или извеждането им към външни системи.

Тези причини ми се сториха съществени поради използването на този подход.

Ще се радвам да отговоря на вашите въпроси, моля пишете коментари.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Първи стъпки с Cloud Firestore за iOS

  2. FrankenQueries:когато SQL и NoSQL се сблъскат

  3. Проследяване на високи CLR_MANUAL_EVENT изчаквания

  4. Актуализации на статистиката

  5. PASS Summit 2013 :Успех в Шарлът