Comments 42
Веб-приложение веб-приложению рознь. У нас тоже была подобная проблема. Один из супругов в командировке в Лондоне, супруга дома в Киеве. Оба одновременно делают запись о расходах в свой общий бюджетный аккаунт. Помимо вопроса «как хранить» есть еще вопрос «как отображать». В нашем случае веб-приложение — полностью на JS а данные приходят в браузер сыром виде. Поэтому хранятся и передаются данные в UTC, а уже в браузере преобразовываются во временную зону ОС. И наоборот: перед отправкой даты временные метки переводятся из локальных в UTC/
Т.е. если супруги запишут одновременно операции (в 1:00 по Киеву и в 23:00 по Лондону) увидят эти операции одновременными, но у супруга они обе будут датированы допустим 17-тым июля, а у супруги — 18-тым.
Собственно по-моему самый логичный подход к проблеме — хранить данные даты и времени в UTC в любом формате, а ввод-вывод уже осуществлять через прямые и обратные преобразования. Что касается преобразований, то тут полагаться на какой-то конкретный инструментарий следует достаточно осторожно, так как данные о преобразованиях для временных зон периодически меняются (как в России в этом году например), а отражается это в изменениях функционала инструментов не всегда достаточно оперативно и адекватно. В частности PHP в этом плане сильно хромает.
В частности PHP в этом плане сильно хромает.

Разве PHP пользуется не системными функциями?
Ну по идее было бы логично если бы он ими пользовался, но как я понял, что для работы с часовыми поясами всё же нет, в windows-варианте по крайней мере.
А можно поподробней про вашу систему преобразования времён в браузере? Как-то это автоматизированно?
Если речь о чем-то вроде date_default_timezone_set — то нет. Но, скажем так, при хорошо спроектированной архитектуре это ведь не проблема. Если за обработку полученных с сервера данных, и за подготовку их к отправке отвечает некоторая прослойка — то совершенно не проблема заставить ее, в случае дат — делать прямое/обратное преобразование с учетом Date().getTimezoneOffset(). А если асинхронность приложения сделана кусочно-гнездовым способом — то тут уж неча на зеркало пенять…
UFO landed and left these words here
Я писал, веб-приложение веб-приложению рознь. Если манипуляции (в том числе отображение) вынесены на клиентскую часть, то как там оно хранится — личное дело сервера. В том смысле, что данные можно (хотя нет — желательно) хранить в некотором унифицированном виде и никак не преобразовывать — только отдавать и записывать. Тогда и пример с супругами в Лондоне и Киеве будет работать логично.
А вот если клиент тонкий, то естественно, часть (или все) подобных манипуляций с датами происходит на сервере. Что вызывает естественное желание спихнуть эту задачу на встроенные возможности того же СУБД.
Ничего не мешает продолжать хранить в datestamp:
SELECT DATE_ADD(date_field, INTERVAL diff HOUR) AS date_field, где diff — разница часовых поясов между пользовательским (в настройках пользователя, либо всего домена сайта) и серверным.
С учетом того, что в России сейчас в плане часовых поясов вечное лето, будет ли по осени вызов
date_default_timezone_set('Europe/Moscow')
корректным?
Или же php эти настройки жестко не вшиты и есть некая системная функция преобразования, которая выдаст корректное значение?
'Europe/Moscow' => +4
Насколько мне известно он и сейчас в разных версиях php является не всегда корректным. На php в этом плане надеяться не стоит.
Кхм, ну как бэ… что тут сказать: довольно долго наблюдал некорректные данные зоны для Новосибирска в свежих версиях php.
А почему на MySQL стоит надеяться? У него принцип тот же самый. И у операционки. Смысл тот же везде пока что — закачиваем базы переходом на летнее/зимнее время и расчитываем сдвиг, исходя из этого.
Конечно принцип везде одинаков, только не всё есть возможность обновлять (виртуальный хостинг бывает в этом плане совсем запущен), не всё одинаково хорошо обновляется. Потому часто применяются «грабли» с использованием комбинации временного смещения и флага использования летнего времени.
Так если не обновлять базу, то откуда брать информацию, что время летнее или зимнее в определенный момент времени?
В смысле? Определить к какому времени относится временная отметка — арифметическая задача, а необходимость добавлять смещение для летнего времени определяется флагом. Не разу не видели реализации? Профиль пользователя имеет настройки часового пояса, но не в виде выбора готовых ID поясов, а в виде указания смещения от UTC и флага указывающего на необходимость автоматического перехода на летнее время.
Не совсем понимаю о каком профиле пользователя идет.
В базе хранится дата/время по UTC: 2011-03-13 00:00:00. Сколько это будет для Лос-Анжелеса? Сколько для Киева? Сколько для Москвы?
Если для известно смещение по времени относительно UTC, известно используется ли в данных местах переход на летнее время (эти данные задаются вручную), то мы легко можем определить относится ли данное время к летнему и вычислить результат, ведь так?
Если нет базы часовых поясов или нет возможности ей воспользоваться или в ней некорректные данные, то как иначе решать задачу? Задавать нужные данные вручную.
Если всё работает правильно и адекватно и своевременно поддерживается, то не надо.
Вызов date_default_timezone_set('Europe/Moscow') будет корректен всегда, пока вы вовремя обновляете пакет tzdata.
обновлять надо с умом — у нас однажды после обновления полностью слетела зона Europe/Moscow — она стала идентична UTC
Побуду я сегодня КО. Существует специальная база данных, хранящая как часовые пояса, так и смещения/переходы на летнее/зимнее время (также называемая базой данных Олсона, по имени автора), причем все из них, т.к. чтобы узнать корректный диапазон между двумя датами, нужно также знать прошлые переходы на летнее/зимнее время. Версия этой базы данных входит в поставку PHP и своевременно обновляется, статус можно посмотреть, например, здесь: www.php.net/manual/en/timezones.php
Россия не единственная такая страна. Многие пояса какое-то время попереключались между летними/зимними временами, а потом перестали. Все это хранится в спец.базах.
Вы не постигли дзен. Смещение часового пояса, которое вы задает для mysql ничего не говорит об этом смещении час или год назад. А есть переходы на зимнее/летнее время, есть перенос часовых поясов. Ничего этого вы не учитываете. Задавать нужно не смещение а саму временную зону, предварительно загрузив в mysql tzdata.
Если вы действительно хотите «избежать головной боли», то не нужно хранить timezone-aware даты в базе данных вообще. Конвертируйте всё в UTC, храните в базе «простую» дату, ничего не знающую о таймзонах, и все вычисления также выполняйте в UTC. И только лишь при отображении данных пользователю конвертируйте их в нужную ему таймзону, на основании пользовательских настроек, используя свежую tzdata естественно.

Да и серверное время, и все настройки PHP рекомендуется в UTC выставлять, дабы не было сюрпризов. Keep it stupid simple, как говорится.
Это не работает при обработке, например, календарных событий, которые почти всегда привязаны к локальному времени.
Проблема еще в том, что поле TIMESTAMP не может быть одновременно не NULL и без ON UPDATE = CURRENT TIMESTAMP…
Может. При создании поля надо явно прописывать DEFAULT. Если же этого не делать и опустить условие ON UPDATE, оно равносильно DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
Вчера с удивлением обнаружил что в Django вопрос с временными зонами тоже через задницу решен. Пришлось извращаться с кастомными полями ORM…

Могу, кстати, посоветовать статью очень подробную и с рецептами asvetlov.blogspot.com/2011/02/date-and-time.html если коротко — внутри приложения всегда(!!!) работаем с UTC, в БД храним в UTC, преобразуем в/из только в момент показа пользователю/получения данных из формы и бед не знаем.
Об этом кто только не писал, вот например последний пост небезызвестного Armin Ronacher о работе с тз в питоне — всё разложено по полочкам (правда на англ.), с какими там полями вам пришлось в джанге извращаться не знаю, если у вас там всё итак в UTC.
Ну это то да, на самом деле во временных зонах все очень просто. Но только если закладывать это с самого начала. Как и с локализацией и с юнит-тестами. Если же внедрять в уже готовое приложение, то затраты будут несравненно больше.

Меня же удивило больше всего то, что даже в таких крупных и известных проектах эта проблема решается как-то никак практически.
Просто стандартная джанговская ORM DateTimeField возвращает naive время. Мне хочется работать с aware — временем, соответственно нужно сделать подкласс этого поля которое автоматом проставляет UTC… Казалось бы просто — ан нет — умная Django для MySQL выдает Exception если ему скормить datetime с проставленной tzinfo а для постгреса сохраняет ТЗ в соответствии с настройкой TIME_ZONE… В общем нужно прилично посидеть и поковыряться чтобы можно было более-менее комфортно работать.
работаем с UTC, в БД храним в UTC, преобразуем в/из только в момент показа пользователю/получения данных из формы и бед не знаем.

Не всегда так можно. Было дело… надо было выбирать из базы данные разбитые по дням. А дни — не по UTC, конечно же :) Проще таки группировать данные уже при выборке.
> в Django вопрос с временными зонами тоже через задницу решен
Согласен. Поэтому пришлось форкнуть django-timezones и серьезно переписать.
github.com/homm/django-timezones/blob/test/timezones/fields.py
Длинные комментарии почти к каждой строчке, чтобы самому не запутаться, что откуда берется.

В моделях то выглядит так:
class City(DomainAttributes):
    TIMEZONE_CHOICES = make_timezone_choices(pytz.country_timezones['ru'])
    timezone = models.CharField(u'Временная зона', max_length=30, choices=TIMEZONE_CHOICES, blank=True, null=True, default=None)
    created = tz_fields.LocalizedDateTimeField(u'Дата создания', auto_now_add=True, default=datetime.now, editable=False, timezone=u'timezone')

class Restaurant(DomainAttributes):
    city = models.ForeignKey(City, verbose_name=u'Город')
    created = tz_fields.LocalizedDateTimeField(u'Дата создания', auto_now_add=True, default=datetime.now, editable=False, timezone=u'city__timezone')

class Order(models.Model):
    restaurant = models.ForeignKey(Restaurant, verbose_name=u'Ресторан')
    created = tz_fields.LocalizedDateTimeField(u'Дата создания', auto_now_add=True, default=datetime.now, editable=False, timezone=u'restaurant__city__timezone')


Поля возвращаются в указанной таймзоне и с привязанной таймзоной. Т.е. их можно безопасно вычитать и сравнивать. При выборке, например, Order не забывайте делать .select_related('restaurant__city'), иначе для каждого заказа LocalizedDateTimeField будет ходить к базе. Правда, вы все равно можете получать created_utc в сыром виде, тогда конечно select_related и поход в базу не нужен.
О, спасибо! Думаю в этом проекте оставлю свои костыли в последующих попробую ваши). Еще-бы README хотябы немного кто-нибудь заполнил ^_^.
Просто у github.com/brosner/django-timezones столько форков, что все их просмотреть и выбрать наиболее доработанный довольно сложно, а основная ветка какая-то недопиленная тоже — как я понял, возвращает это поле все-равно naive datetime.

> Поля возвращаются в указанной таймзоне
В таймзоне проставленной в City.timezone?

> вы все равно можете получать created_utc в сыром виде
Ну вот это по-моему самый удачный вариант. В сочетании с фильтром для шаблона, умеющем приводить ТЗ к пользовательскому, должно довольно хорошо работать.
Если у вас таймзона привязана не к городам, а к пользователям, то вам для поля нужно задать не лукап timezone=u'restaurant__city__timezone', а колбак, который вернет что нужно. Одна проблема — в Джанге сложно получить request и пользователя в произвольной функции, но все же возможно.
1. Не создавайте себе проблем (хранение не UTC дат), тогда не нужно будет их преодолевать (масштабное переписывание кода и SQL запросов).
2. Пост конечно нужный. Как раз для случая, когда первое не соблюдалось.
Only those users with full accounts are able to leave comments. Log in, please.