Как стать автором
Обновить

Комментарии 33

То, что для хранения в БД нужно использовать DateTime.UtcNow вместо DateTime.Now — это как минимум сильно спорно. Микрооптимизации не должны влиять на логику работы, ведь это буквально разное время (на какой-то момент показалось, что автор оригинала живёт в часовом поясе UTC+0 и ему всё равно). Да и является ли это вообще оптимизацией, если потом для отображения надо будет каждый раз время переводить в локальное?

И уж как минимум в данном контексте стоило упомянуть о DateTimeOffset.

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

Так себе пример, но логику отразить может.

Именно для этого и используется DateTimeOffset. Вы предлагаете сделать соглашение, что всё время — в utc+0 и это будет работать, но ничто никому не помешает нарушить это соглашение и «по старинке» использовать просто Now, и тогда начнётся погружение в удивительный мир отладки распределённых приложений, особенно интересно выглядящий с точки зрения времени/часовых поясов.

Когда данные собраны в базу из разных источников, в подавляющем большинстве случаев неважно знать, какой часовой пояс использовал пользователь/система в момент отправки данных. В тех случаях, когда это важно, DateTimeOffset вполне оправдан. В других - скорее избыточен, т.к. занимает вдвое больше места.

Когда данные собраны в базу из разных источников, в подавляющем большинстве случаев неважно знать, какой часовой пояс использовал пользователь/система в момент отправки данных.
Можете привести какую-то статистику, или это просто мнение, взятое с потолка?

Так или иначе, это очевидно, что хранить ненужные данные — избыточно. Но в этой ветке речь идёт о том, когда часовой пояс важен.

Так что вы хотите сказать, в контексте? Что нужно использовать DateTime.UtcNow и терять то время, что было у пользователя, потому что это неважно знать? Или что нужно конвертировать каждый раз в локальное время? Или что в подавляющем случае время неважно и вообще надо на него забить?

Скажем так, я не настаиваю, что это единственно правильное решение, но считаю рациональным "нормализовать" входные данные, приводя их к нулевому часовому поясу (да, UtcNow), а при отображении - приводить к часовому поясу пользователя.

Аналог: если система поддерживает различные единицы измерения, вы вряд ли будете хранить в одном столбце "19 м", " 140 дюймов", "4 фута". Скорее всего, всё будет преобразовано к одной единице измерения, а потом оттбражатся в виде, удобном пользователю. Исключение - когда это приводит к недопустимой потере точности; в таком случае хранение в разных единицах оправдано.

Исходных данных для этого обсуждения обычно гораздо больше.
Перед продолжением холивара посмотрите, например, вот эти статьи:
https://habr.com/ru/post/278527/
https://habr.com/ru/company/vk/blog/242645/

Мне грустно повторяться, но выше я уже написал, что такая нормализация будет хорошо работать. Ровно до момента, пока кто-то, кто не в курсе (а, согласитесь, это рано или поздно наступит) не начнёт записывать просто DateTime.Now. И тогда у нас какой-нибудь список событий превратится в кашу, когда создание товара было после его отправки. Если вас не слопают пользователи на этом или вы готовы к такому повороту, учитывая, что отлаживать это не так-то просто, а взять информацию о том, какое значение корректное неоткуда — хорошо.

Давайте тогда поймём, зачем нам такая красота нужна. Вы пишете, что мы вдвое экономим место. Тут нужно уточнение, где именно. Например, в postgresql типы timestamp with timezone и timestamp without timezone занимают одинаково — 8 байт. Если речь про память, то DateTimeOffset занимает 12 байт для 32-битных систем и 16 байт для 64-битных, когда DateTime — 8 байт везде. Если такая экономия оправдана, то я полностью согласен — это оптимизация.

Стоит ли она озвученных мной рисков — инженерное решение, но я бы рекомендовал использовать DateTimeOffset по умолчанию. Всё же в реальных промышленных приложениях лучше пожертвовать несколькими байтами памяти, чем долго и мучительно страдать, пытаясь откопать истоки проблем с этими проклятыми датами.
Я может быть не совсем понял аргумента, но ведь таким же образом кто-то может начать записывать и DateTime.UtcNow, случайно перепутав. Почему ошибка может быть совершена в сторону Now, но не наоборот?
Разумеется, и в эту сторону тоже может быть ошибка. Поэтому и вариант решения — не использовать соглашения, а использовать явные часовые пояса.
НЛО прилетело и опубликовало эту надпись здесь
Часовым поясам вполне можно верить, просто нужно понимать, что смещение относительно UTC — это не вся необходимая информация, есть ещё локальное регулирование, тут уже нужно знать именование зоны, что не поддерживает DateTimeOffset, но поддерживает NodaTime.

Да, часовые пояса — это сложно, и если у вас будет подобная задача про соотношение исторических данных на обширных территориях, то вы действительно пройдёте все круги ада и стадии принятия, но у вас есть шанс успешно выполнить задачу.

Если у вас всё в UTC+0, то уже час ночи по Москве станет у вас предыдущей датой, и круги ада начнутся сильно раньше. То, что часовые пояса — это сложно всё-таки не повод от них отказываться совсем.
НЛО прилетело и опубликовало эту надпись здесь
Я с вами разговариваю аргументами — а вы пытаетесь в ответ «задавить авторитетом». Я вам говорю — да, я знаю, часовые пояса — это сложно, но не повод же отказываться от них. Вы мне в ответ — вон есть свойство, это точно повод отказаться от часовых поясов!
Это как бы намекает что местное время (с часовым поясом) не всегда может однозначно определять момент во времени.
В каких ситуациях в вашей практике такое случалось? Как много записей из миллиона будут такими?
Серьезно, следуйте рекомендуемым практикам и не стреляйте себе в ногу.
Договорились, буду следовать рекомендациям:
DateTimeOffset

Use this structure to work with dates and times whose offset (or difference) from UTC is known. The DateTimeOffset structure combines a date and time value with that time's offset from UTC. Because of its relationship to UTC, an individual date and time value unambiguously identifies a single point in time. This makes a DateTimeOffset value more portable from one computer to another than a DateTime value.
НЛО прилетело и опубликовало эту надпись здесь

Мне кажется вы ставите все с ног на голову. Если кто-то "забыл", то это не проблема архитектуры и подхода. Так можно "забыть" вообще дату писать, да и много еще чего ;-) обычно это называют багом.

А для распределенных систем использование UTC для взаимодействия нормальная практика. Хороший пример календарь. Зачем хранить время с привязкой к таймзоне? Сегодня я в Питере, а завтра уже во Фриско. И живу я по локальным часам. Зачем мне знание таймзоны в которой я вводил данные, мне нужны данные в локальной таймзоне.

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

И то, что большую часть времени всё будет работать хорошо — сделает вам трудности в отладке. По данным у вас всё будет прекрасно. Или нет? Вы будете видеть, что товар пришёл ночью, хотя по факту он пришёл днём. Это кто-то где-то отправил Now? Может, кто-то залез в БД руками? Или всё правильно, но часовой пояс в том месте, где произошло событие такой, что в UTC+0 получилась другая дата? Во время отладки надо ещё не забыть про этот момент, который «не проблема подхода».

И тут — эврика, вы понимаете в чём проблема! Только решить вы её уже не сможете, у вас весь массив данных не содержит информации о часовых поясах.

Мысль с календарным днём натолкнула меня на свою неправоту раньше: нет, UtcNow не будет работать, даже в случаях, когда все следуют одному правилу. Если нужны часовые пояса — используйте DateTimeOffset.

Прелесть DateTime заключается в том, что он ещё и хранит флажок, показывающий, является ли время локальным или в UTC. И грамотно написанный API проверит этот флажок и сделает необходимые преобразования.

Вы о каких преобразованиях? Давайте на примере, в БД хранится значение, скажем, DateTime = 10/21/2021 12:04:52 AM, Kind — Local.
Расскажете, как грамотно написать API, чтобы получить UTC+0?
Давайте на примере, в БД хранится значение, скажем

Неверно. В БД хранятся даты только в UTC.


Вы о каких преобразованиях?

Local -> UTC при работе с API базы. API принимает дату в любом формате, но делает преобразование при необходимости. Или вы предлагаете в базу писать напрямую, минуя API?

Local -> UTC при работе с API базы. API принимает дату в любом формате, но делает преобразование при необходимости. Или вы предлагаете в базу писать напрямую, минуя API?

1. Как он узнает, что есть необходимость? Пришла вам DateTime в виде строки с фронта. Тот же «10/21/2021 12:04:52 AM». Там нет никакого флажка, показывающего тип даты. Что делаем?

2. API будет один-единственный на все даты системы?

Мы же о типе DateTime из C# сейчас говорим, не? Если вам дата пришла в виде строки, то очевидно, что она имеет тип "строка", а не DateTime.


Ну а если ваше приложение, написанное на C#, сериализовало дату в подобном виде, то ССЗБ. Нормальный API должен принимать дату исключительно в формате ISO и слать нахрен тех, кто шлёт дату чёрти как.

Мы же о типе DateTime из C# сейчас говорим, не? Если вам дата пришла в виде строки, то очевидно, что она имеет тип «строка», а не DateTime.
А вам на API данные каким образом приходят? Не строками, интерпретируемыми платформой разве из json? Похоже, мы о чём-то разном говорим.
Ну а если ваше приложение, написанное на C#, сериализовало дату в подобном виде, то ССЗБ. Нормальный API должен принимать дату исключительно в формате ISO и слать нахрен тех, кто шлёт дату чёрти как.
Хорошо, переформулирую по ISO 8601: 2021-10-21T12:04:52. Какое это время в utc+0?
А вам на API данные каким образом приходят? Не строками, интерпретируемыми платформой разве из json? Похоже, мы о чём-то разном говорим.

Нет, не строками. Я, например, MessagePack использую с сериализацией DateTime.ToBinary().


Хорошо, переформулирую по ISO 8601: 2021-10-21T12:04:52. Какое это время в utc+0?

Запрещаем передачу времени-даты без явного указания временной зоны (или Z).

Запрещаем передачу времени-даты без явного указания временной зоны (или Z).
Вот мы и пришли к временным зонам. Также, конечно, вы далеко не всегда диктуете форматы.

Но мне интереснее то, как вы работаете с ситуациями, когда изменение на UTC+0 меняет хранимую дату?

Ну конкретном в моём случае допустима только передача времени в UTC-формате. При строковой сериализации обязателен суффикс "Z", указывающий на UTC.

Local -> UTC при работе с API базы. API принимает дату в любом формате, но делает преобразование при необходимости.

То есть API для работы с базой данных должно понимать любые форматы? И уметь их как-то распознавать и преобразовывать в UTC?


На мой взгляд всё-таки логичнее оставить в API только UTC(или какой-то другой формат, но один единственный). И пусть все кто пользуются этим API сами конвертируют свои форматы в нужный и обратно.

Данные это ведь не только логи, к примеру в большой компании есть установленный рабочий день от 9 до 18 по местному времени, и нам надо собрать переработки.

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

p.s. вот с тем что использовать now для замера производительности нет особого смысла, полностью согласен....

Почему бы в данном случае не использовать на стороне базы данных тип даты-времени с сохранением часового пояса (`timestamp with time zone` / timestamptz в Postgres)?

Получится красиво: и сохранение локального времени независимо от локации, и возможность отображать его в произвольном часовом поясе, и правильная сортировка по данному полю.

Правда, не уверен, сохраняет ли тип DateTime в C# часовой пояс.

DateTime не имеет вообще информации о часовом поясе, только суррогат в виде Kind-а. Для timestamptz обычно используют DateTimeOffset либо вообще (что, кстати правильней) NodaTime.

Читать тяжеловато (но автор перевода тут, конечно, не виноват), идёт 4 абзаца где разными словами говорится «разработчики не знают, как правильно», и один абзац по делу.

Удивлён, что кто-то не знает о том, что время в БД должно быть в UTC. Причин полно. Распределенные системы в разных поясах, перевод часов, наличие пользователей в разных часовых поясах. Конечно, речь в первую очередь про системные поля, типа createdAt, updatedAt и пр., которые участвуют в каких-то синках данных, на которые мы смотрим в логах при поиске причин ошибок. Наверное, для десктопной программы utc уже не так критичен, как и в случае поля времени, которое вообще ни на что не влияет - дело выбора.

Кроме того, в проекте должен быть ровно один вызов DateTime.UtcNow. В неком сервисе. А остальной код берет время у него. Если везде вызывать DateTime напрямую, такой код сложнее покрыть тестами, если время в них важно (проверяем, например, что через 5 минут после события система ведёт себя так-то, и конечно не тест 5 минут висит, а просто сдвигаем его в замоканном/зафейконом сервисе времени). Ну и в целом недетерменипованность для тестов - плохо, использование реального времени системы одна из причин недетерменированности.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий