README.md

java-timezone-example

Демо-приложение на базе Spring Boot 3 с демонстрацией сохранения временных отметок в PostgreSQL БД с сохранением временного сдвига.

Описание проблемы

В PosgtreSQL в столбцах с типом TIMESTAMP WITH TIME ZONE значение на самом деле хранится в UTC. Если сохраняемое значение принадлежит другой временной зоне, то перед сохранением значение принудительно переводится в UTC. Подробнее см. в документации к PostgreSQL.

Это приводит к потере информации о временном сдвиге после сохранения данных в БД.

Решение

Предложено использовать возможности ORM-фреймворка Hibernate 6, который используется в Spring Boot версии 3 и выше.

Данная версия Hibernate умеет сохранять поля с временными значениями в два поля таблицы: в одном собственно значение, в другом - значение временного сдвига. Для программиста задача сводится к созданию дополнительного столбца и добавлению следующей аннотации к полю в JPA сущности:

@TimeZoneStorage(TimeZoneStorageType.COLUMN)

Для того, чтобы корректно работала сортировка по исходному полю, было решено оставить его тип TIMESTAMP WITH TIME ZONE. В этом случае временные значения будут сортироваться согласно их UTC-представлению, что соответствует хронологическому порядку.

Дополнительное поле должно по умолчанию иметь название с суфиксом _tz и иметь тип INTEGER. Таким образом, наши поля в БД выглядят следующим образом:

ts    TIMESTAMP WITH TIME ZONE NOT NULL,
ts_tz INTEGER                  NOT NULL,

При выборке записей с сортировкой по полю ts Hibernate добавляет в секцию ORDER BY оба поля:

select *
from sample
order by ts, ts_tz

Конечно, для хронологического порядка добавление поля ts_tz не требуется, достаточно было бы сортировать только по полю ts, но так уж работает Hibernate. Возможно, это можно как-то подкорректировать.

Для эффективной сортировки индекс, создаваемый для поля ts, должен также включать поле ts_tz, в противном случае PostgreSQL не будет его использовать:

CREATE INDEX ix_sample_ts ON sample (ts, ts_tz);

Сборка приложения

./mvnw clean verify

Упаковка в Docker-контейнер

docker image build -t tzdemo:1.0.0 -t tzdemo:latest . 

Запуск приложения

docker compose up

При успешном запуске приложение будет слушать HTTP-соединения по порту 8080.

Тестирование

После запуска приложения создадим три записи с временными отметками из разных временных зон:

curl --location 'http://localhost:8080/api/samples' \
--header 'Content-Type: application/json' \
--data '{
    "ts": "2023-10-26T12:34:56+03:00",
    "val": "+3"
}' && \
curl --location 'http://localhost:8080/api/samples' \
--header 'Content-Type: application/json' \
--data '{
"ts": "2023-10-26T12:34:56+04:00",
"val": "+4"
}' && \
curl --location 'http://localhost:8080/api/samples' \
--header 'Content-Type: application/json' \
--data '{
"ts": "2023-10-26T12:34:56+05:00",
"val": "+5"
}'

Теперь прочитаем ранее созданные записи:

curl --location 'http://localhost:8080/api/samples'

Записи возвращаются в хронологическом порядке, значения временных сдвигов сохранены:

[
  {
    "ts": "2023-10-26T12:34:56+05:00",
    "val": "+5"
  },
  {
    "ts": "2023-10-26T12:34:56+04:00",
    "val": "+4"
  },
  {
    "ts": "2023-10-26T12:34:56+03:00",
    "val": "+3"
  }
]

Также проверим обратный порядок сортировки:

curl --location 'http://localhost:8080/api/samples?reversed=true'

Записи возвращаются в обратном хронологическом порядке:

[
  {
    "ts": "2023-10-26T12:34:56+03:00",
    "val": "+3"
  },
  {
    "ts": "2023-10-26T12:34:56+04:00",
    "val": "+4"
  },
  {
    "ts": "2023-10-26T12:34:56+05:00",
    "val": "+5"
  }
]

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

explain analyze select * from sample order by ts, ts_tz;

QUERY
PLAN                                                        
-------------------------------------------------------------------------------------------------------------------------
 Index Scan using ix_sample_ts on sample  (cost=0.15..61.95 rows=920 width=60) (actual time=0.023..0.027 rows=3 loops=1)
 Planning Time: 0.114 ms
 Execution Time: 0.081 ms
(3 rows)

Посмотрим, как выглядят наши данные в БД:

# select ts, ts_tz, val from sample;

           ts           | ts_tz | val 
------------------------+-------+-----
 2023-10-26 09:34:56+00 | 10800 | +3
 2023-10-26 08:34:56+00 | 14400 | +4
 2023-10-26 07:34:56+00 | 18000 | +5

Как видим, значение временного сдвига хранится в секундах.

Описание

Демо-проект на Spring Boot 3 и Hibernate 6, демонстрирующий хранение временных отметок с указанием временного сдвига в PostgreSQL

Конвейеры
0 успешных
0 с ошибкой