Database
 sql >> Baza danych >  >> RDS >> Database

Więcej o wprowadzeniu stref czasowych w długowiecznym projekcie

Jakiś czas temu zaczęliśmy dostosowywać system do nowego rynku, który wymaga obsługi stref czasowych. Wstępne badania zostały opisane w poprzednim artykule. Teraz podejście nieco ewoluowało pod wpływem realiów. W tym artykule opisano problemy napotkane podczas dyskusji i ostateczną decyzję, która została wdrożona.

TL;DR

  • Konieczne jest rozróżnienie terminów:
    • UTC to czas lokalny w strefie +00:00, bez efektu czasu letniego
    • DateTimeOffset – lokalne przesunięcie czasu względem UTC ± NN:NN, gdzie przesunięcie jest przesunięciem podstawowym względem UTC bez efektu czasu letniego (w C# TimeZoneInfo.BaseUtcOffset)
    • DateTime – czas lokalny bez informacji o strefie czasowej (ignorujemy atrybut Kind)
  • Podziel użycie na zewnętrzne i wewnętrzne:
    • Dane wejściowe i wyjściowe za pośrednictwem interfejsu API, wiadomości, eksporty/importy plików muszą być w formacie UTC (typ DateTime)
    • W systemie dane są przechowywane wraz z przesunięciem (typ DateTimeOffset)
  • Podziel użycie starego kodu na kod inny niż DB (C#, JS) i DB:
    • Kod inny niż DB działa tylko z wartościami lokalnymi (typ DateTime)
    • Baza danych działa z wartościami lokalnymi + offset (typ DateTimeOffset)
  • Nowe projekty (komponenty) używają DateTimeOffset.
  • W bazie danych typ DateTime po prostu zmienia się na DateTimeOffset:
    • W typach pól tabeli
    • W parametrach procedur składowanych
    • Niezgodne konstrukcje zostały naprawione w kodzie
    • Informacja o przesunięciu jest dołączona do otrzymanej wartości (prosta konkatenacja)
    • Przed powrotem do kodu innego niż DB, wartość jest konwertowana na lokalną
  • Brak zmian w kodzie innym niż DB
  • DST jest rozwiązywany za pomocą procedur składowanych CLR (w przypadku SQL Server 2016 można użyć AT TIME ZONE).

Teraz bardziej szczegółowo o pokonanych trudnościach.

„Głęboko zakorzenione” standardy branży IT

Dużo czasu zajęło uwolnienie ludzi od strachu przed przechowywaniem dat w czasie lokalnym z offsetem. Jakiś czas temu, jeśli zapytasz doświadczonego programistę:„Jak obsługiwać strefy czasowe?” – jedyną opcją było:„Użyj czasu UTC i przekonwertuj na czas lokalny tuż przed demonstracją”. Fakt, że do normalnego przepływu pracy nadal potrzebujesz dodatkowych informacji, takich jak nazwy przesunięcia i strefy czasowej, był ukryty pod maską implementacji. Wraz z pojawieniem się DateTimeOffset takie szczegóły wyszły na jaw, ale bezwładność „doświadczenia programistycznego” nie pozwala szybko zgodzić się z innym faktem:„Przechowywanie lokalnej daty z podstawowym przesunięciem UTC” jest tym samym, co przechowywanie UTC. Kolejną zaletą używania DateTimeOffset wszędzie jest delegowanie kontroli nad przestrzeganiem stref czasowych .NET Framework i SQL Server, pozostawiając ludzkiej kontroli tylko momenty wprowadzania i wyprowadzania danych z systemu. Kontrola człowieka to kod napisany przez programistę do pracy z wartościami daty/czasu.

Aby przezwyciężyć ten strach, musiałem odbyć więcej niż jedną sesję z wyjaśnieniami, prezentacją przykładów i Proof of Concept. Im prostsze i bliższe przykłady zadań rozwiązanych w projekcie, tym lepiej. Jeśli zaczniesz dyskusję „ogólnie”, prowadzi to do komplikacji w zrozumieniu i marnowania czasu. Krótko mówiąc:mniej teorii – więcej praktyki. Argumenty dla czasu UTC i przeciwko DateTimeOffset mogą być powiązane z dwiema kategoriami:

  • „UTC cały czas” to standard, a reszta nie działa
  • UTC rozwiązuje problem z DST

Należy zauważyć, że ani UTC, ani DateTimeOffset nie rozwiązują problemu z DST bez wykorzystania informacji o regułach konwersji między strefami, które są dostępne za pośrednictwem klasy TimeZoneInfo w C#.

Model uproszczony

Jak wspomniałem powyżej, w starym kodzie zmiany zachodzą tylko w bazie danych. Można to ocenić na prostym przykładzie.

Przykład modelu w 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'

Wynik wykonania skryptu będzie następujący.

Przykład pokazuje, że model ten pozwala na dokonywanie zmian tylko w bazie danych, co znacznie zmniejsza ryzyko defektów.

Przykłady funkcji do przetwarzania wartości daty/czasu

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

Małe artefakty

Podczas dostosowywania kodu SQL znaleziono pewne rzeczy, które działają dla DateTime, ale są niezgodne z DateTimeOffset:

GETDATE()+1 należy zastąpić DATEADD (dzień, 1, SYSDATETIMEOFFSET ())

Słowo kluczowe DEFAULT jest niezgodne z DateTimeOffset, musisz użyć SYSDATETIMEOFFSET()

Konstrukcja ISNULL(date_field, NULL)> 0″ działa z DateTime, ale DateTimeOffset należy zastąpić wartością „date_field IS NOT NULL”

Wnioski lub UTC vs DateTimeOffset

Ktoś może zauważyć, że podobnie jak w podejściu z UTC, mamy do czynienia z konwersją podczas odbierania i zwracania danych. Więc po co nam to wszystko, skoro istnieje sprawdzone i działające rozwiązanie? Istnieje kilka powodów takiego stanu rzeczy:

  • DateTimeOffset pozwala zapomnieć, gdzie znajduje się SQL Server.
  • Pozwala to przenieść część pracy do systemu.
  • Konwersję można zminimalizować, jeśli DateTimeOffset jest używany wszędzie, wykonując ją tylko przed wyświetleniem danych lub wysłaniem ich do systemów zewnętrznych.

Te powody wydawały mi się niezbędne dzięki zastosowaniu takiego podejścia.

Chętnie odpowiem na Twoje pytania, pisz komentarze.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Tworzenie prostej aplikacji internetowej za pomocą funkcji Bottle, SQLAlchemy i Twitter API

  2. Kilka sposobów na wstawienie dzielonych ciągów rozdzielanych w kolumnie

  3. Wprowadzenie do IRI Voracity (całkowite zarządzanie danymi) [wideo]

  4. Obsługa baz danych SQL za pomocą PyQt:podstawy

  5. Klauzula SQL GROUP BY dla początkujących