Даты и время - кажется, простые на первый взгляд понятия, но они могут быть источником множества тонкостей и ошибок при разработке программного обеспечения. Прежде чем углубляться в детали, я хочу подчеркнуть ключевое допущение, которое лежит в основе всей этой статьи.
Если вы считаете, что конкретика и ясность в коде важнее устных или письменных соглашений между разработчиками, то этот текст для вас. Иначе, многие из приведенных далее аргументов могут показаться несущественными или излишними.
То есть, я полагаю, что логика и поведение программы лучше всего фиксировать и контролировать на уровне кода или типов, а не на уровне соглашений. Это предоставляет более высокий уровень уверенности в корректности программы и уменьшает вероятность возникновения ошибок из-за недопонимания или невнимательности.
Определение абсолютного и относительного времени
Работая с датами и временем, мы часто сталкиваемся с концепциями, которые, кажется, просты и понятны, но могут вносить путаницу без четкого определения. Особенно это касается понимания часовых поясов и взаимосвязи между местным и универсальным временем. Прежде чем углубиться в эту тему, введем два ключевых понятия: абсолютное и относительное время.
Абсолютное время — это время, которое не зависит от часовых поясов, географического положения или индивидуальных предпочтений. Оно едино и неизменно для всего мира в каждый конкретный момент, а потому оно абсолютно. Это своего рода "базовое" время, часто ассоциируемое со Скоординированным Всемирным Временем (UTC).
Относительное время — в отличие от абсолютного, относится к конкретному часовому поясу и зависит от географического расположения. Оно отражает реальное местное время, адаптированное к условиям конкретного региона. Например, когда по UTC 12:00, в Москве будет 15:00 (учитывая разницу в 3 часа).
Таким образом:
- Абсолютное время — единый стандарт, на основе которого происходит синхронизация времени по всему миру, он абсолютен, так как не зависит от внешних факторов и является универсальным для всех точек мира.
- Относительное время — это местное время, которое относится к конкретному месту и соответствует реальному дню и ночи в данной локации.
Когда мы говорим об абсолютном и относительном времени в контексте программирования и баз данных, имеется в виду, сохраняется ли информация о часовом поясе или нет. В PostgreSQL, для представления времени с и без информации о часовом поясе существуют разные типы данных.
-
Абсолютное время (с информацией о часовом поясе):
timestamp with time zone(обычно аббревиатураtimestamptz): Хранит момент времени, который учитывает часовой пояс. PostgreSQL автоматически преобразуетtimestamptzв UTC при сохранении и обратно в локальное время при извлечении на основе настроек сервера или сессии.
-
Относительное время (без информации о часовом поясе):
timestamp without time zone(обычно аббревиатураtimestamp): Хранит момент времени без учета часового пояса. Это просто дата и время, без относительности к какому-либо часовому поясу.date: Хранит только дату без времени и часового пояса.time: Хранит только время без даты и часового пояса.
Timestamp в PostgreSQL сохраняет дату и время исключительно как они были введены, не привязывая их к определенному часовому поясу. Это значит, что если в базе данных сохранен timestamp как 2023-09-25 12:00:00, он всегда будет отображаться именно так, не зависимо от настроек часового пояса сервера или сессии пользователя.
Пример:
Предположим, у нас есть timestamp 2023-09-25 12:00:00. Если часовой пояс сервера GMT+3, то значение всё равно останется 2023-09-25 12:00:00.
В отличие от timestamp, timestamptz учитывает часовой пояс, конвертируя введенное значение времени в UTC при сохранении. При запросе к базе данных PostgreSQL конвертирует это время из UTC в часовой пояс сервера или сессии, на основе текущих настроек.
Пример:
Записываем 2023-09-25 12:00:00 как timestamptz в часовом поясе GMT+3. PostgreSQL сохранит это значение как 2023-09-25 09:00:00 UTC. При запросе, если часовой пояс сервера GMT+5, значение будет отображаться как 2023-09-25 14:00:00.
Однако время из такого поля можно запросить и в UTC формате, тогда оно будет иметь формат 2023-09-25T09:00:00Z где Z в конце указывает, что дата указана в UTC.
По умолчанию, timestamptz отображает время в часовом поясе сервера, однако, его можно перенастроить для отображения в другом часовом поясе, используя параметры сессии или конфигурации сервера.
Timestampсохраняет локальное время, без учета часового пояса.Timestamptzсохраняет время в UTC и конвертирует его в локальное время в зависимости от настроек часового пояса при запросе.- Настройка часового пояса может быть изменена на уровне сервера или сессии, что влияет на отображение времени для
timestamptz.
-
Абсолютное время:
- В формате ISO 8601 с указанием часового пояса, например:
2023-09-23T10:00:00+03:00. Здесь "+03:00" указывает на часовой пояс. Также часто используют формат UTC, например:2023-09-23T07:00:00Z. - В виде Unix time (или Epoch time): это количество секунд, прошедших с полночи 1 января 1970 года по UTC. Например, значение
1601234567представляет определенный момент времени, который можно преобразовать в удобочитаемый формат.
- В формате ISO 8601 с указанием часового пояса, например:
-
Относительное время:
- В формате ISO 8601, но без указания часового пояса, например:
2023-09-23T10:00:00. - Только дата в формате
YYYY-MM-DD, например:2023-09-23. - Только время в формате
HH:MM:SS, например:10:00:00.
- В формате ISO 8601, но без указания часового пояса, например:
GraphQL структура и типизация данных играют ключевую роль. Четкое определение каждого типа данных гарантирует не только корректное взаимодействие между клиентом и сервером, но и устойчивость системы к ошибкам, связанным с неправильной интерпретацией данных.
-
Абсолютное время: Важно выбрать один стандарт для абсолютного времени, чтобы обеспечить единообразие во всей системе.
DateTimeUtc: Тип, представляющий время в формате ISO 8601 в UTC. Например,2020-04-20T16:20:04.000000Z.DateTimeWithZone: Тип, представляющий время в формате ISO 8601 с указанием часового пояса. Например,2023-09-23T10:00:00+03:00.UnixTimestamp: Тип для представления времени в формате Unix time. Например,1601234567.
-
Относительное время:
DateTime: Тип для представления даты и времени без часового пояса в формате ISO 8601. Например,2023-09-23T10:00:00.Date: Тип, представляющий только дату в формате ISO 8601. Например,2023-09-23.Time: Тип, представляющий только время в соответствии со стандартом ISO 8601. Например,10:00:00.
Создавая такие четко определенные типы в GraphQL, вы устанавливаете строгие границы на уровне контракта для передачи и интерпретации данных. Это обеспечивает снижение риска ошибок при работе с временем и упрощает интеграцию с клиентскими приложениями, гарантируя стабильное взаимодействие во всей системе.
Основная идея: Определить, где и когда выбирать абсолютное или относительное время в зависимости от бизнес-логики.
Скорее всего, если поле времени автоматически устанавливает сервер, то это будет абсолютное время. Если время устанавливает пользователь (мануально), то в большинстве случаев это будет относительное время.
Однако стоит отметить, что в случае если пользователю будет предложено предоставить информацию через сколько он например будет, то нам надо будет работать с абсолютным временем
-
Отель:
created_at: Абсолютная дата и время добавления отеля на платформу. Устанавливается сервером. Тип:DateTimeUtc.updated_at: Абсолютная дата и время последнего изменения информации об отеле. Устанавливается сервером. Тип:DateTimeUtc.check_in_fromиcheck_out_until: Относительное время заезда и выезда в отеле. Устанавливается представителем отеля (мануальный ввод). Тип:Time.
-
Номер в отеле:
availability_dates: Относительные даты доступности номера. Устанавливается на основе дат указанных гостями (мануально) при своих бронированиях. Тип:Date[].
-
Бронирование:
booking_timestamp: Абсолютная дата и время создания бронирования. Устанавливается сервером. Тип:DateTimeUtc.stay_from_dateиstay_until_date: Относительные даты пребывания гостя. Устанавливается гостем. Тип:Date.stay_from_timeиstay_to_time: Время заезда и выезда гостя. Устанавливается гостем. Тип:Time.
У сущности бронирования, изменение даты и времени имеют разный бизнес смысл, а значит и логику к ним необходимо будет привязывать разну. Разделяя дату и время, нам становится проще привязывать разное поведение на изменение этих полей по отдельности.
-
Изменение даты: Нам необходимо пересчитать стоимость бронирования исходя из новых дат, по суточному тарифу
-
Изменение времени прибытия: проверяем соответствует ли выбранное время стандартам отеля (
check_in_fromиcheck_out_until) и, если необходимо, обрабатываем дополнительные сценарии, такие как ранний заезд или дополнительная оплата.
Таким образом, разделив дату и время на два разных поля ввода, мы обеспечиваем разделение бизнес логики на уровне данных
Соблюдение описанных выше принципов позволяет разработчикам, работающим как на клиентской, так и на серверной стороне, исключить множество типичных проблем, связанных с часовыми поясами. Это достигается за счет того, что вся работа с часовыми поясами учитывается и решается на уровне архитектуры хранения данных, а также благодаря инструментам стандартной библиотеки большинства языков программирования и систем управления базами данных.
Следуя рекомендациям из статьи, мы избавляемся от следующих проблем:
- Исключается влияние серверного времени на то, какое время будет отображаться у пользователя на клиентской стороне.
- Нет необходимости конвертировать даты в определенный часовой пояс, так как это учтено заранее.
- В целом, вопросы конвертации времени и работы с различными часовыми поясами перестают быть актуальными для разработчика.
Таким образом, следуя рекомендациям из статьи, можно сосредоточиться на основной логике приложения, минимизировав возможные ошибки и сложности, связанные с временем и датами.