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

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

Спасибо за инструмент. Он до сих пор вызывает некоторые проблемы у меня, IDEA при каждом своем перезапуске просит настроить его, но в любом случае вижу тенденцию к полезности вашего плагина.

Спасибо! А можно поподробнее про настройку при каждом перезапуске? Напишите пожалуйста на info@jpa-buddy.com, будем очень благодарны!

Когда я запускаю IDEA, то в правом нижнем углу она показывает разные информационные маленькие окошки, вот оно из окошек гласит "Вы используете hibernate, а не хотите ли настроить плагин под него" (сообщение на английском, но смысл вот такой). Я нажимаю настроить, выходит окно установки плагина, где показывается ваш плагин и мне нужно только нажать Ок. В принципе, не критично, но вот обратил внимание на такое поведение. На указанную почту скину версии всего используемого и скрины

Понял-понял. "Мопед не наш" :). Это окно из IntelliJ, попробуем что-то с этим сделать! Спасибо!

Для начала, спасибо вам за отличный инструмент, пользуемся в команде им активно!

Но вот по поводу маппинга timestamp у меня для вас огорчение. Ответ в твиттере был верный! Заренее извиняюсь за портянку текста, просто не раз уже вижу такое заблуждение.

Простой timestamp – это метка времени "не привязанная" к временной зоне, это именно LocalDateTime. Timestamp with timezone – это метка "привязанная" к временной зоне, это метка в глобальном течении времени, и это Instant / OffsetDateTime.

Timestamp можно замаппить на Instant только если у нас бекенды приложения в одной временной зоне.

Давайте рассмотрим связку java и postgres, в котором работа с датой / временем сделана по канону. При отправке значения timestamp оно форматируется в строку с временной зоной бекенда. Сервер postgres принимает эту строку и разбирает ее в целое число. Внутреннее представление timestamp / timestamp with tz – это простое int64. Разница лишь в том, что при разборе timestamp переданная в строке TZ не учитывается, а при разборе timestamp with tz временная зона учитывается и значение приводится к UTC.

При вычитке значения из бд идет обратное преобразование: для timestamp временная зона не передается и бекенд "предполагает" свою, а для timestamp with tz временная зона клиента (бекенда) учитывается и время выравнивается сервером бд по ней.

В итоге, если есть бекенд А во временной зоне GMT+05 и бекенд Б в зоне GMT+10, то при передаче с бекенда А простого timestamp (без TZ) 10:00:00+05 на сервере БД сохранится 10:00:00 и при вычите на бекенд Б придет тоже 10:00:00, но только на Б "предполагается" уже GMT+10. А это никак не метка в глобальном течении времени. Это никак не Instant. Это просто LocalDateTime.

Спасибо большое, что не поленились и все написали :)!

Да, я понимаю ваш кейс. Мы так изначально и думали. Но все же в общем случае это именно инстатнт. Давайте посмотрим на примеры.

Мы ставим отметку времени получения данных - это именно момент времени или Instant. Мы фиксируем дату и время принятия документов в посольство и это Instant. Фиксируем дату нарушения ПДД - Instant. Т.е. это характеристика некоторого события. И, как выяснилось, это и есть основные кейсы.

Кейсы, подходящие под вашу логику такие: Студент должен подать документы на поступление в ВУЗ до 10:00 утра. И таквезде, т.к. это распоряжение МинОбраза. В Москве, Самаре, Воронеже,... Это и есть LocalDateTime. И, количественно, по нашим изысканиям, такие кейсы уступают упомянутым выше.

Однако, было понятно, что такой подход устроит не всех. Вот вас например не устроил :). Поэтому предусмотрена возможность переопределить дефолтный маппинг. Описано это это в доке: https://www.jpa-buddy.com/documentation/database-versioning/#custom-type-mappings. Просто добавьте туда маппинг, который считаете правильным и все.

Еще один способ, когда ваши колонки в базе типа timestamp интерпритируется "то так, то так", то можно вборочно обойтись добавлением columnDefinition.

Мы ставим отметку времени получения данных - это именно момент времени или Instant. Мы фиксируем дату и время принятия документов в посольство и это Instant. Фиксируем дату нарушения ПДД - Instant. Т.е. это характеристика некоторого события. И, как выяснилось, это и есть основные кейсы.

Все верно, это метки в глобальном течении времени. Для таких случаев надо хранить метку как timestamp with timezone. Если будем хранить метку как обычный timestamp, то в случае нескольких бекендов в разных временных зонах получим неразбериху.

Я понимаю ваш подход – покрыть более частые кейсы, а для особых мест иметь возможность переопределить. Но я считаю инструмент должен давать советы "как правильно делать", а не советы рода "как чаще используется в жизни".

Ведь вопрос с маппингом дат касается не только reverse-engineering, но и генерации migration на основе наших Entity. И правильно делать маппинг LocalDateTime -> timestamp, Instant -> timestamp with tz. Но сейчас насколько вижу для Instant генерится простой timestamp, что как я выше пояснил может породить неразбериху в случае бекендов в разных временных зонах.

Честно, я не понимаю почему это может привести к неразберихе, даже если бэкеды будут в разных таймзонах. Время из инстанта не пересчитывается в локальную таймзону сервера. Вот прямо сейчас сделал тест. В обеих базах ровно одно и то же значение, как и при чтении. Попробовал Oracle, PostgreSQL 13, MS SQL.

Ведь вопрос с маппингом дат касается не только reverse-engineering, но и генерации migration на основе наших Entity.

Все так, поэтому настроить можно и там и там :).

Время из инстанта не пересчитывается в локальную таймзону сервера.

Все верно, но сервер БД пересчитывает в таймзону клиента (бекенда).

Проверил на postgres и на, прости господь, h2database, работает как я и ожидал. https://github.com/vasyakolobok77/test_jpa

Извиняюсь за тесты с простым логированием, но суть они показывают. Давайте разберем, что происходит на примере postgres:
1. Допустим Бекенд А находится в зоне GMT+05 и по его часам сейчас 18:00, что по UTC будет 13:00.
2. Бекенд А сохраняет таймштамп в БД. В postgres, как я описывал ранее, timestamp передается простой строкой с таймзоной бекенда, т.е. с бекенда придет условно 18:00+05 (опустим дату и миллисекунды).
3. Сервер принимает строку и просто отбрасывает временную зону +05, к себе он сохранит 18:00.
4. Далее мы решаем прочитать сохраненное значение на бекенд А. Запрашиваем значение у БД и она отвечает нам строкой 18:00.
5. Бекенд принимает 18:00 и интерпретирует его в своей временной зоне +05, в итоге объект Instant будет указывать на 13:00Z (что равно 18:00+05).
6. Далее мы решаем прочитать сохраненное значение с другого бекенда Б, который допустим в зоне GMT+10. Бекенд запрашивает у БД и она отвечает все той же строкой 18:00.
7. Бекенд Б принимает 18:00 и интерпретирует это значение в своей временной зоне +10, что порождает объект Instant 08:00Z (что равно 18:00+10).
8. 13:00Z на бекенде А и 08:00Z на бекенде Б совсем не ссылаются на один момент времени. Они указывает на локальное время 18:00 :-)

По описанному алгоритму работает postgres и h2db. С другими БД каюсь не проверял.

В тестах spring нужно учитывать, что контекст / коннекции к БД переиспользуется, потому простой setTimeZone в тест методах не прокатит, нужно делать полный перезапуск приложения в другой временной зоне.

Ээээ. Так все же верно. Сохраняем с клиента инстант 29.01.2022 15:40 UTC. Ровно это значение получаем в базе. Ровно это знаение и зачитываем из базы. Таймзоны для работы в UTC вообще не причем. Их просто нет. Всегда +00.

Что значит "бэкенд интерпретирует это значение в своей временной зоне"?

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

Еще раз, если в зоне +05 мы сохраняем условную дату 18:00+05 (13:00+00) как таймштамп, то оно сохранится просто как 18:00. В spring+hibernate сохранение Instant вырождается в setTimestamp, в котором передается строковое представление даты в своей временной зоне. Никакого UTC там нет в помине. Postgres при обработке timestamp проигнорит таймзону +05 и сохранит просто 18:00. Посмотрите в отладчике.

При вычитке на бекенде этого значения идет обратное преобразование. Опять же postgres передаст просто 18:00, а spring+hibernate распарсят строку и применят текущую временную зону! В итоге в каждой временной зоне получим 18:00, но это будут свои метки: 18:00+05 и 18:00+10 , что соответствует меткам в глобальном времени 13:00+00 и 08:00+10 !!! Это разные метки.

Еще раз, timestamp в общем случае может хранить толкьо локальное время. Для хранение глобального времени (чем является Instant) нужен timestamp with tz.

Надеюсь я понятно теперь пояснил. Если у вас есть возражения, с удовольствием выслушаю.

Что значит "бэкенд интерпретирует это значение в своей временной зоне"?

Чтобы преобразовать строку даты / времени "2022-01-30T18:00:00.000" в Instant нужно знать временную зону. Для timestamp типа postgres не передает временную зону, потому бекенд использует текущую. В итоге для одного локального времени в каждой таймзоне получаем разный инстант. Об этом я говорю уже несколько комментариев.

Я тут распинаюсь в доказательствах, но хочу уточнить. Надеюсь вы не используете Instant как замену LocalDateTime? оО Поскольку если это так (что в корне неправильно), то дискуссия у нас ни о чем. :-)

Надеюсь вы не используете Instant как замену LocalDateTime?

Очевидно что нет.

Для timestamp типа postgres не передает временную зону, потому бекенд использует текущую.

Чтобы не было проблем работы в разных таймзонах и не было преобразование времени из локали JVM в локаль DB и обратно есть такое замечательное свойство: spring.jpa.properties.hibernate.jdbc.time_zone=UTC.

есть такое замечательное свойство: spring.jpa.properties.hibernate.jdbc.time_zone=UTC

Согласен, с таким workaround (по факту костылем!) Instant работает как надо, но перестает работать маппинг LocalDateTime в timestamp.

Если в одном проекте у нас и Instant для хранения глобальных меток, и LocalDateTime для хранения локальных меток, то не получится их одновременно замаппить на timestamp, что-то одно сломается.

Честно, на костыль похоже подстановка локальной тамзоны, при явном отсутствии таковой. Есть даже статья Влада Михалчи, где про это явно говорится. Еще он упоминает, что далеко не все драйвера БД проставляют локальную тамзону. Вся история учит нас отказываться от дефолтов, причем плавающих (зависящих от подключенных зависимостий например).

Мне пришлось много поработать со временными полями и должен сказать, что практически всегда в итоге приходилось отказываться от локального времени вообще. Либо храним в UTC и переводим в нужную локаль на презентационном слое, либо храним полностью с часовым поясом. В противном случае мы завязываемся на внешние факторы. Например, непредсказуемые переводы времени. Да-да, бывает и такое. Когда все сервера думают, что надо переводить время с 2am на 1am, а локальные законы (это было с проектом в Ливане) делают это с 1am на 12pm (т.е. фактически переводят и день). И все заказы такси летят к чертям :). Также были случаи подения серверов, а при переподнятии невыставление правильной локали.

Спасибо вам большое за терпение, я проведу еще несколько консультаций со сторонними экспертами, чтобы окончательно решить как нам быть :). Так-то поменяем, в чем проблема :).

И вам спасибо за беседу, спасибо, что выслушали. :-)

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