Database
 sql >> Base de Dados >  >> RDS >> Database

Mais sobre a introdução de fusos horários no projeto de longa duração


Há algum tempo, começamos a adaptar o sistema ao novo mercado que exige suporte para fusos horários. A pesquisa inicial foi descrita no artigo anterior. Agora, a abordagem evoluiu ligeiramente sob a influência das realidades. Este artigo descreve os problemas encontrados durante as discussões e a decisão final que é implementada.


TL;DR

  • É necessário distinguir os termos:
    • UTC é a hora local na zona +00:00, sem o efeito DST
    • DateTimeOffset – deslocamento de hora local do UTC ± NN:NN, em que o deslocamento é o deslocamento base do UTC sem o efeito do horário de verão (em C# TimeZoneInfo.BaseUtcOffset)
    • DateTime – hora local sem informações sobre o fuso horário (ignoramos o atributo Kind)
  • Divida o uso em externo e interno:
    • Dados de entrada e saída via API, mensagens, exportações/importações de arquivos devem estar estritamente em UTC (tipo DateTime)
    • Dentro do sistema, os dados são armazenados junto com o deslocamento (tipo DateTimeOffset)
  • Divida o uso do código antigo em código não-DB (C#, JS) e DB:
    • O código não DB opera apenas com valores locais (tipo DateTime)
    • O banco de dados funciona com valores locais + deslocamento (tipo DateTimeOffset)
  • Novos projetos (componentes) usam DateTimeOffset.
  • Em um banco de dados, o tipo DateTime simplesmente muda para DateTimeOffset:
    • Nos tipos de campo de tabela
    • Nos parâmetros de procedimentos armazenados
    • Construções incompatíveis são corrigidas no código
    • As informações de deslocamento são anexadas a um valor recebido (concatenação simples)
    • Antes de retornar ao código não DB, o valor é convertido para local
  • Nenhuma alteração no código não DB
  • DST é resolvido usando CLR Stored Procedures (para o SQL Server 2016 você pode usar AT TIME ZONE).



Agora, com mais detalhes sobre as dificuldades que foram superadas.

Padrões "enraizados" do setor de TI


Demorou muito tempo para aliviar as pessoas do medo de armazenar datas na hora local com deslocamento. Algum tempo atrás, se você perguntar a um programador experiente:“Como oferecer suporte a fusos horários?” – a única opção era:“Use UTC e converta para hora local antes da demonstração”. O fato de que para o fluxo de trabalho normal você ainda precisa de informações adicionais, como os nomes de deslocamento e fuso horário, estava oculto sob o capô da implementação. Com o advento do DateTimeOffset, tais detalhes surgiram, mas a inércia da “experiência de programação” não permite concordar rapidamente com outro fato:“Armazenar uma data local com um deslocamento UTC básico” é o mesmo que armazenar UTC. Outra vantagem de usar DateTimeOffset em todos os lugares permite delegar o controle sobre a observância dos fusos horários do .NET Framework e do SQL Server, deixando para o controle humano apenas os momentos de entrada e saída de dados do sistema. O controle humano é o código escrito por um programador para trabalhar com valores de data/hora.

Para superar esse medo, tive que realizar mais de uma sessão com explicações, apresentação de exemplos e Prova de Conceito. Quanto mais simples e próximos forem os exemplos daquelas tarefas que são resolvidas no projeto, melhor. Se você começar na discussão “em geral”, isso leva a uma complicação de entendimento e perda de tempo. Resumidamente:menos teoria – mais prática. Os argumentos para UTC e contra DateTimeOffset podem ser relacionados a duas categorias:
  • “UTC o tempo todo” é o padrão e o resto não funciona
  • UTC resolve o problema com DST

Deve-se notar que nem UTC nem DateTimeOffset resolvem o problema com DST sem usar informações sobre as regras de conversão entre zonas, que estão disponíveis através da classe TimeZoneInfo em C#.

Modelo simplificado

Como observei acima, no código antigo, as alterações acontecem apenas em um banco de dados. Isso pode ser avaliado usando um exemplo simples.

Exemplo de um modelo em 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'

O resultado da execução do script será o seguinte.



O exemplo mostra que esse modelo permite fazer alterações apenas no banco de dados, o que reduz significativamente o risco de defeitos.

Exemplos de funções para processar valores de data/hora
// 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

Pequenos artefatos

Durante o ajuste do código SQL, foram encontradas algumas coisas que funcionam para DateTime, mas são incompatíveis com DateTimeOffset:

GETDATE()+1 deve ser substituído por DATEADD (dia, 1, SYSDATETIMEOFFSET ())

A palavra-chave DEFAULT é incompatível com DateTimeOffset, você precisa usar SYSDATETIMEOFFSET()

A construção ISNULL(date_field, NULL)> 0″ funciona com DateTime, mas DateTimeOffset deve ser substituído por “date_field IS NOT NULL”

Conclusão ou UTC vs DateTimeOffset


Alguém pode notar que, como na abordagem com UTC, lidamos com a conversão ao receber e retornar dados. Então, por que precisamos de tudo isso, se existe uma solução bem testada e funcionando? Há várias razões para isso:
  • DateTimeOffset permite que você esqueça onde o SQL Server está localizado.
  • Isso permite transferir parte do trabalho para o sistema.
  • A conversão pode ser minimizada se DateTimeOffset for usado em todos os lugares, realizando-a apenas antes de exibir dados ou enviá-los para sistemas externos.

Essas razões me pareceram essenciais devido ao uso dessa abordagem.

Será um prazer responder suas perguntas, por favor escreva comentários.