Pull to refresh

Comments 62

«Хранение локальной даты с базовым смещением от UTC» — это тоже самое, что хранение UTC
Нет, это больше, чем просто UTC. В России часто происходит дурдом с изменением зон в разных городах, типа давайте с нового года жить в UTC+03 вместо UTC+04, или давайте отменим летнее время, потом снова его введём, потом снова отменим.

Поэтому, например, для программы кадрового учёта, которая хранит время сотрудников на работу, UTC — лишние проблемы (если посмотреть на 2 года в прошлое, 5:00/UTC — это опоздание на час или приход вовремя?)

Конвертации можно свести к минимуму, если DateTimeOffset используется везде, делая их только перед отображением данных
Тут всё равно нужен анализ ситуаций. Иногда время 15:00 MSK показывать в Нью-Йорке как есть (с припиской «по Москве»), иногда конвертить в местное.
UFO just landed and posted this here
Так и есть. Просто адепты UTC пропускают этот факт: все равно надо будет обеспечить хранение смещения. Толька в случае с UTC all the time самостоятельно с вытекающими из этого «напрягами».
UFO just landed and posted this here
Имеет смысл или нет зависит от проекта. Если у вас не было поддержки таймзон, то переписывать в любом случае придется. И если сравнивать два подхода с UTC-all-the-time и DateTimeOffset, то при равных затратах на переписывание, второй проще технически.
Таймзона это безусловно дополнительная информация ко времени события. Но описанный случай со временем прихода на работу является практически исключительным, и в этом случае я бы даже хранил таймзону как отдельное поле ) чтобы всем было понятно что эта информация которую нельзя потерять при всяких конвертациях ) Но если вам необходимо просто абсолютное время произошедшего события то таймзона лишняя информация которой просто не должно быть на уровне БД.
Используя принцип Оккама не умножать сущности без необходимости )
Вывод времени события в таймзоне пользователя относится к презентационной логике и конвертация в локальное время пользователя по моему скромному времени должна быть только там а не в хранимых процедурах и не в каком либо t-sql.

То же самое относится к загрузкам данных, у разных источников данных могут быть разные соглашения о формате времени и таймзоне, но задачу конвертации в один общий формат и одну таймзону БД лучше решать именно там в каждом конкретном сервисе загрузки данных.
Кстати таймзона БД не обязательно должна быть UTC, иногда удобнее хранить данные в таймзоне локальной для основной команды разработки. Но главное чтобы к этой таймзоне не применялись правила DST. То есть если EST так уж EST никаких там ESD.
Что то мне подсказывает то все эти проблемы и изыскания от непонимания того, что такое «UTC» и что такое «Часовой пояс» (Timezone). А самое главное, как правильно это использовать.
единственным вариантом был: «Используй UTC, а конвертируй в локальное время только перед показом»

Он и остался единственным правильным, и сводящим всевозможные проблемы конвертации к минимуму.
Тот факт, что для нормальной работы все равно необходима дополнительная информация, такая, как смещение и названия таймзон, был спрятан под капотом реализации.

Для нормальной работы клиент(т.е. пользователь) должен так или иначе сообщить системе, в каком часовом поясе и в какой стране он находится. Иначе любые танцы с бубном не имеют ни какого смысла.

Чтобы не было никаких проблем достаточно 3 вещи:
1. Время везде хранись в UTC
2. Знать список правил, как устанавливать смещение относительно UTC (В зависимости от часового пояса, времени года, страны)
3. Знать Часовой пояс пользователя, а так же страну его местонахождения.

Если я не прав, рад бы был услышать возможные проблемы в данном подходе.
Проблему я описал в первом комментарии. Она возникает, когда для события важно знать локальное время и показывать даты в часовой зоне события, а не в часовой зоне клиента, отображающего данные.
Этот конкретный юзкейс решается хранением данных о том, где и в каком часовом поясе это событие произошло.
Причем для любого кол-ва событий произошедших географически в одном месте достаточно хранить всего 1 значение с информацией о том, где и в каком часовом поясе событие произошло.

В физическом же смысле не важно где произошло событие, если оно произошло, то произошло одновременно во всех существующих часовых поясах.
для любого кол-ва событий произошедших географически в одном месте достаточно хранить всего 1 значение с информацией о том, где и в каком часовом поясе событие произошло
Это неудобно, т.к. придётся к этому месту привязывать историю изменений его часового пояса. Гораздо прозрачнее текущую таймзону сохранять вместе с событием.
> В физическом же смысле не важно где произошло событие, если оно произошло, то произошло одновременно во всех существующих часовых поясах.

В физическом смысле (раз уж вы его вспомнили) эта часть ваших комментариев тоже некорректна, т.к. в этом случае информация о событии должна была бы передаваться быстрее скорости света в вакууме.

А по топику — ваш подход к UTC, как «единственному правильному» способу хранения дат — пример упрощённого и шапкозакидательского подхода к работам с датами. Благодаря таким же чувакам мне сейчас приходится на смартфоне использовать временную зону Кувейта, из-за долговременной стабильности оной (и невозможности часто просто указать фиксированную временную зону как смещение). То тут, то там оказывается, что кто-то использовал то или иное умолчание, которое, почему-то, оказалось некорректным.

По факту, термины: 1) время в UTC, 2) время в локальной временной зоне (политически установленной), 3) время в временной зоне ±IJKL, где IJKL — цифры, 4) время в т.н. «floating» временной зоне — описывают разные явления нашей жизни. И подход «используй везде UTC», как минимум, вследствие этого факта, неверен.

P.S. Также неверно путать системное время в компьютере (в т.ч. в СУБД и в других серверах) и время как физическую абстракцию/модель. В частности, в компьютере время не монотонно возрастающее. :)
В частности, в компьютере время не монотонно возрастающее. :)
Я подзабыл значение монотонный, вы имеете в виду что в компьютере дискретно?

Монотонность — отсутствие движения значения функции/элементов последовательности в другом направлении. Дискретность — это другая беда, общая для программирования вообще.

Я удивлен. Запросто можно получить одно и то же время на том же компьютере, но кажется я не встречал что бы таймер шел назад. Связан ли обратный отсчет с несколькими процессорами? Может связан с NUMA…

Он связан с возможностью крутить системные часы в любую сторону. Этим занимается либо пользователь — либо служба синхронизации времени (на линуксе — ntpd). А еще иногда на материнке дохнет батарейка.

Благодаря таким же чувакам мне сейчас приходится на смартфоне использовать временную зону Кувейта, из-за долговременной стабильности оной

А для меня такие чуваки, на моем смартфоне запилили галочку «Устанавливать время автоматически»
И вот уже 10 лет как со временем у меня нет проблем.
А по топику — ваш подход к UTC, как «единственному правильному» способу хранения дат — пример упрощённого и шапкозакидательского подхода к работам с датами

Этот поход придумал не я, его годами выверяли и стандартизировали. Но в вашем мировоззрении конечно все дурачки, напридумывали всякой фигни. Лучше уж свои велосипеды строить. К слову даже в автоматике в системах реального времени хренят и используют время в UTC, потому что намного меньше проблем с тем чтобы преобразовать время перед отправкой клиенту, и не преобразовывать его больше вообще нигде.
А для меня такие чуваки, на моем смартфоне запилили галочку «Устанавливать время автоматически»
И вот уже 10 лет как со временем у меня нет проблем.

Попробовал поставить на своем смартфоне такую галочку. Время резко изменилось на час. Выключил. Поверьте, автоматика не всегда работает, или забыли про историю со сменой часовых поясов в России, как на многих смартфонах будильники не сработали?

Этот поход придумал не я, его годами выверяли и стандартизировали. Но в вашем мировоззрении конечно все дурачки, напридумывали всякой фигни

Понимаете, в чем дело — у подобных стандартов есть область применения, и не всегда стоит их слепо применять. Всегда нужно думать о своей задаче. Например, для задачи «контролировать опоздания работников» локальное время важно, а UTC и таймзоны нет. Решать задачу через несвязанные с ней абстрактные (хоть и стандартные) инструменты — несколько неправильно, не находите?
Главная проблема datatype'ов с поддержкой таймзон — отсутствие единого железного стандарта о том, как они должны работать. Как видим, даже внутри SQL Server поведение типа DateTimeOffset немного отличается от DateTime.

Что будет, если понадобится выгрузить эти данные для обработки в Hive, в BigQuery, в системы аналитики, в визуализаторы (Tableau etc.)? Что будет, если захотим поменять СУБД целиком?
UFO just landed and posted this here
Это артифакты доставшиеся из далекого прошлого. Было любопытно их обнаружить.

Полностью согласен с коллегой DexterHD. Если все данные в системе хранятся и обрабатываются в UTC, то остается всего одна проблема — определить желаемую временнУю зону при представлении этих данных конечному пользователю. Другие варианты не обладают универсальностью и хороши лишь в определенных условиях и до тех пор, пока эти условия не изменятся.

Прямо «неделя проповедничества DateTimeOffset» на Хабре…

На Хабре вопрос временных зон обсуждается регулярно и давно уже известно, что надо хранить время так, чтобы метод хранения решал поставленные задачи. Например, в какой зоне хранить дату "через месяц в три часа дня"? В текущей? В будущей? В той, которая будет принята правительством через две недели?


Для абстрактного проекта действительно даты надо хранить либо в UTC, либо в формате с указанием смещения от UTC. Это позволит в крайнем случае скачать базу данных временных зон и получить местное время — почти всегда, так как местное время иногда устанавливается волевым решением руководства лодочной станции. Это позволяет синхронизировать события. Это устраняет некоторые странные проблемы. Но пример выше показывает, что в некоторых случаях это недостаточно. Просто надо помнить что время — самая сложная концепция в программировании (после выбора названия для объектов).

Действительно инертность есть. Не хочется перелопачивать код в DateTimeOffset. Просмотрел статью, не убедила в использовании этого другого подхода. Может быть кто-нибудь кратко объяснит, в чем преимущество этого способа перед «везде внутри используем UTC»?
Например, следующие вещи меня убедили в том, что DateTimeOffset лучше чем UTC all the time.

Меньше волнений из-за:
  • меньше волнений из-за сервера БД, т.к. он может работать в любой зоне (если кто-то случайно поменял ее — пофиг)
  • меньше кода проверок таймзоны входящих значений (например, например, проверка всяких Kind и действительно ли пришедшая дата в зоне N, а не UTC?)
  • проще соглашения по использованию дат внутри системы (не надо постоянно заботиться о передаче UTC)
  • проще обработка данных из разных таймзон, т.к. многие рутинные вещи выполняет фреймворк
  • новые C# проекты могут вовсе заботиться о конвертации только в момент, когда выводится пользователю. А сохранять в БД значения со смещением, которое предоставляется система. Т.е. разрабу вообще не надо будет думать о всяких конвертациях и UTC пока он не возвращает данные наружу (экран или внешние системы)

Простой переезд, т.к. в БД DateTimeOffset кастится к DateTime и обратно без ошибок, только отрезается информация о смещении. Поэтому на время переезда вы спокойно можете сосредоточиться только на базе, не трогая C#. Дат, обычно немного.

НО:
При чтении SQL DateTimeOffset в C# DateTime возникает исключение. Затраты на компенсацию соизмеримы с затратами по поддержке UTC. При этом смещение вам, скорее всего, все равно понадобится хранить рано или поздно. Только с подходом UTC вы обрекаете себя на дополнительные муки. Так же не стоит забывать о проблемах, которые связаны с DateTime.

Простите, но какие проблемы могут в принципе возникнуть при смене временной зоны сервера в подходе "all UTC"? Зачем проверять Kind если известно, что все даты — в UTC?

Потому что DateTime приводится к UTC в зависимости от Kind. Если проскочит неверный, то возникнет ошибка, которую вы просто так не отловите. Например, кто-то забыл выставить Kind, или наоборот выставил, когда не надо было. Вручную конвертацию производить? Без четкого обозначения смещения, вы обрекаете девелопера помнить об этом постоянно. А потом еще и тикеты из саппорта разгребать.
Про сервер. Потому что значения, сохраненные, как бы, в UTC после переезда сервера в другую зону, тоже переедут, т.к. рассчитывались относительно таймзоны смещения сервера. Задачу можно решить, но опять же, за счет больших напрягов для девелоперов: необходимо использовать только правильные методы работы с датами.

Да с чего им переезжать-то?!

Не понял вопрос. Кому «им»?

Датам, кому же еще. Вот лежит у меня в базе дата в UTC. Я меняю у сервера тайм-зону. Что, по-вашему, случится с датой?

Давайте рассмотрим случай:
— из базы достается ДТ значение без оффсета (DateTime)
— в коде C# вы конвертируете это в зону юзера, предполагая, что пришли UTC

1) Проблема с чтением из БД
var utc = new DateTime(2017, 4, 4, 10, 0, 0); // from DB expecting UTC
var targetTz = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
var local = TimeZoneInfo.ConvertTime(utc, targetTz);

Console.WriteLine("utc: {0}", utc);
Console.WriteLine("local: {0}", local);


Результат:
utc: 04/04/2017 10:00:00
local: 04/04/2017 10:00:00


Решение: Надо поставить Kind в Utс, или использовать TimeZoneInfo.ConvertTimeFromUtc().

2) Проблема с датами при сохранении
Суть примера: юзер вводит два раза одну и туже дату, но первую он вводит пока сервер в одной таймзоне, а вторую — когда в другой. Затем сервер пытается привести все к дате юзера и терпит неудачу, т.к. они не равны. А должны быть одинаковые.

var server1Timezone = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
var server2Timezone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");

var userInput1 = new DateTime(2017, 4, 4, 13, 0, 0);
var utc1 = TimeZoneInfo.ConvertTimeToUtc(userInput1, server1Timezone);

var userInput2 = new DateTime(2017, 4, 4, 13, 0, 0);
var utc2 = TimeZoneInfo.ConvertTimeToUtc(userInput2, server2Timezone);

var targetTz = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
var local1 = TimeZoneInfo.ConvertTimeFromUtc(utc1, targetTz);
var local2 = TimeZoneInfo.ConvertTimeFromUtc(utc2, targetTz);

Console.WriteLine("display: {0}", local1);
Console.WriteLine("display: {0}", local2);

Результат:
display1: 04/04/2017 13:00:00
display2: 04/04/2017 20:00:00

Следует учесть, что такие строчки, как TimeZoneInfo.ConvertTimeToUtc(userInput1, server1Timezone); будут выполняться самой системой, т.е. это придется как-то решать. И если пропустите, то никто не упадет и не скажет об ошибке. Например, в SQL и в C# у вас могут использоваться такие вещи, которые оперируют с локальной таймзоной сервра. Например, GETDATE() или TimeZoneInfo.ConvertTime(). Вам надо будет постоянно следить за правильностью кода.

DateTimeOffset убирает «предположения» из работы с датами.

С какого перепугу у вас дата, введенная пользователем, интерпретируется исходя из тайм-зоны сервера?

Это же только пример, показывающий природу неоднозначности DateTime.

В таком случае, вот это — пример, показывающий неоднозначность типа данных int:


var x = 42;
x = x+1;
Console.WriteLine($"{x} = 42");
Речь о том, что для правильной работы с UTC-all-the-time вам надо постоянно помнить о том, что:
— какая зона у юзера
— правильно ли я произвел конвертацию
— правильно ли передался Kind в момент, когда это важно

C DateTimeOffset вам придется об этом думать только в момент перед выдачей результата (на экран или во внешнюю систему).

Не нужно это все помнить. Подход UTC-all-the-time подразумевает, что конвертация производится ровно 1 раз, на границе подхода. Надо просто сделать ее и забыть.

Такой оптимизм рождает потом баги.

Баги рождает не оптимизм, а отсутствие соглашений.

Решение: Надо поставить Kind в Utс, или использовать TimeZoneInfo.ConvertTimeFromUtc().

Так и используйте, в чем проблема?

первую он вводит пока сервер в одной таймзоне, а вторую — когда в другой

Таймзона сервера тут ни при чем. Должно быть примерно так:

var userTimezone = getUserTimeZoneFromSomeStorage();

var userInput1 = new DateTime([params from user input]);
var utc1 = TimeZoneInfo.ConvertTimeToUtc(userInput1, userTimezone);

var userInput2 = new DateTime([params from user input]);
var utc2 = TimeZoneInfo.ConvertTimeToUtc(userInput2, userTimezone);

// save to DB

...

// show data from DB

var utc1 = new DateTime([params from DB]);
var utc2 = new DateTime([params from DB]);

var userTimezone = getUserTimeZoneFromSomeStorage();

var local1 = TimeZoneInfo.ConvertTimeFromUtc(utc1, userTimezone);
var local2 = TimeZoneInfo.ConvertTimeFromUtc(utc2, userTimezone);

Console.WriteLine("display: {0}", local1);
Console.WriteLine("display: {0}", local2);


В БД положили, и оно там лежит. Изменение таймзоны БД на сохраненные данные не влияет. Изменение таймзоны сервера на выполняющийся код не влияет. Таймзона пользователя в пределах запроса одна и та же. Все выборки с фильтрами или вычисления в бизнес-логике можно делать в UTC, конвертирование только при выводе.
Никто не спорит, что с UTC это можно. Но зачем руками, когда DateTimeOffset позволяет все тоже самое с меньшими усилиями?
А какой код будет с DateTimeOffset?

А какой код будет с DateTimeOffset, если одни и те же данные десятиминутной давности могут смотреть 2 пользователя в разных таймзонах?
Я к тому, что в статье у вас вроде то же самое, только на SQL. Зачем использовать связку SQL + C#, если можно использовать просто C#?
Потому что при использовании C# вы будете делать компенсирующие вещи, такие как конвертация в UTC и обратно, учет Kind и прочее. При «связке» вам ничего ненадо делать, т.к. работаете так, словно зон нет ровно до момента вывода данных. Я обобщаю, конечно, т.к. работать надо будет с DateTimeOffset, что чуть-чуть иначе, чем с DateTime.
Я меняю у сервера тайм-зону. Что, по-вашему, случится с датой?
Когда есть куча legacy-кода, в котором есть вызовы DateTime.Now и GETDATE(), причём ещё как-то неявно (например, из сторонних компонент), то смена зоны сервера приложений или сервера БД станет проблемой.

Если сразу всё писалось и тестилось с DateTime.UtcNow и GETUTCDATE(), то проблемы не будет.
UFO just landed and posted this here
UFO just landed and posted this here
Как я понял, Вы говорите только о случае, когда вы десериализуете значение, но упускаете из виду ситуацию, когда десериализованная дата передается в другой компонент, который тоже имеет логику вокруг таймзон.
Мы использовали очень похожий подход. Спасибо за статью

Жесткий vendor lock, обратная несовместимость, неполнота локальной информации, а реальных преимуществ нет.
Пример с опозданием на работу в любом случае требует привязки к конкретному офису и его расположению — в эту информацию таймзона обязательно входит.
Пока полученный результат больше всего похож на велосипед с квадратными колесами.
Что только люди ни придумают лишь бы UTC не хранить.

Пример с опозданием на работу в любом случае требует привязки к конкретному офису и его расположению — в эту информацию таймзона обязательно входит.
Однако, таймзона в Усть-Урюпинске сейчас и тайм-зона там же 2 года назад — совершенно разные вещи ))) И нормального надёжного способа получить её нет, разве что парсить местные газеты с местными указами.

Распорядок дня с привязкой к конкретной организации, должности, смене тоже сам собой не нарисуется. Зону за прошлое получить не проще и не сложнее. А вот что в смещении после указов Медведева были ошибки и много — 99%. Таймзону уточнить и исправить достаточно однажды, а вот со смещениями придется повозиться.
Даже в идеальном для авторов идеи варианте получается не айс.

Есть велосипеды со специальными колесами для езды по ступенькам и на них действительно удобно ездить по ступенькам.


image


Если человеку, который всю жизнь ездит только по ступенькам, предложить велосипед с круглыми колесами, то он возмутится. И будет прав. Потому что по ступенькам неудобно ездить на круглых колесах. И он будет до тех пор отстаивать преимущества квадратных колес, пока не попробует покататься на них по ровной поверхности и сравнить с круглыми.

UFO just landed and posted this here

Реальные велосипеды реального мира, если что:
image

Что только люди ни придумают лишь бы UTC не хранить.

DateTimeOffset эквивалентно хранению UTC. При условии, что используется базовое смещение от UTC.

Только зачем он нужен?
Усложнение, ломающее изменение, избыточная информация, отсутствие поддержки от большинства СУБД.
И все это ради вариантов использования, которые все равно требуют дополнительной информации о месте события, в которое все равно входит таймзона.
Про время же события необходимо и достаточно хранить UTC.
Получается простое, чистое и элегантное сочетание преждевременной оптимизации с денормализацией.

Если сразу делать в UTC, это можно.
А вот для миграции (причём на лету, без остановки) системы с длинной историей развития несовместимые типы очень кстати — видно, где уже поправили, а где ещё нет.

Если специально нужен несовместимый тип то в нем надо хранить UTC без всякой дополнительной мишуры.
DateTimeOffset — удобная вещь для расчетов, но не для хранения.

Sign up to leave a comment.

Articles