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 с ошибкой