Работа с ZonedDateTime в связке postgres + hibernate + spring data

ZonedDateTime — это класс восьмой джавы для представления даты/времени в определенной временнОй зоне. Это может быть запуск ракеты или выход нового релиза программы ZonedDateTime является immutable, т.е. при операциях с объектами класса создается новый объект.
Postgres предоставляет все средства для работы с такими данными.

Для полей ZonedDateTime в БД будем использовать тип TIMESTAMP WITH TIME ZONE.

Вам точно понадобится конвертер:

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.TimeZone;
 
@Converter(autoApply = true)
public class ZonedDateTimeConverter implements AttributeConverter<ZonedDateTime, Calendar> {
    @Override
    public Calendar convertToDatabaseColumn(ZonedDateTime entityAttribute) {
        if (entityAttribute == null) {
            return null;
        }
 
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(entityAttribute.toInstant().toEpochMilli());
        calendar.setTimeZone(TimeZone.getTimeZone(entityAttribute.getZone()));
        return calendar;
    }
 
    @Override
    public ZonedDateTime convertToEntityAttribute(Calendar databaseColumn) {
        if (databaseColumn == null) {
            return null;
        }
 
        return ZonedDateTime.ofInstant(databaseColumn.toInstant(),
                databaseColumn
                .getTimeZone()
                .toZoneId());
    }
}

(Обратите внимание, что ZonedDateTime приводится к типу Calendar.)

который используем так:

import javax.persistence.Convert;
...
    @Column(name = "resolution_date")
    @Convert(converter = ZonedDateTimeConverter.class)
    private ZonedDateTime resolutionDate;

Cериализатор:

[ваше имя пакета]
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
 
import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
 
public class ZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
 
    @Override
    public void serialize(ZonedDateTime dateTime, JsonGenerator generator, SerializerProvider provider)
            throws IOException {
 
        String dateTimeString = dateTime.format(
                DateTimeFormatter
                .ISO_INSTANT
                .withZone(ZoneId.systemDefault()));
        generator.writeString(dateTimeString);
    }
}

Десериализатор:

[ваше имя пакета]
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
 
import java.io.IOException;
import java.time.ZonedDateTime;
 
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
 
public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
 
    private static final String NULL_VALUE = "null";
 
    @Override
    public ZonedDateTime deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException {
        ObjectCodec oc = jp.getCodec();
        JsonNode node = oc.readTree(jp);
        String dateString = node.textValue();
 
        ZonedDateTime dateTime = null;
        if (!NULL_VALUE.equals(dateString)) {
            dateTime = ZonedDateTime.parse(dateString, ISO_DATE_TIME);
        }
        return dateTime;
    }
}

Приведу пример использования ZonedDateTime в параметрах методов репозитория spring data:

[ваше имя пакета]
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.format.annotation.DateTimeFormat;
 
    @RestResource
    List<Topic> findByResolutionDateBetweenAndAuthorId(
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @Param("resolutionDateLower") ZonedDateTime resolutionDateLower,
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @Param("resolutionDateUpper") ZonedDateTime resolutionDateUpper,
            @Param("authorId") Long authorId);

Как можно понять из названия метода, он находит список объектов Topic, у которых дата находится в промежутке (resolutionDateLower, resolutionDateUpper) и у которых в качестве автора указан конктретный пользователь.

Протестировать можно так:

        List<Payment> list = paymentRepo.
                findByResolutionDateBetweenAndAuthorId(
                ZonedDateTime.parse("2017-01-23T00:00:00.000Z", DateTimeFormatter.ISO_DATE_TIME),
                ZonedDateTime.parse("2017-01-23T23:59:59.999Z", DateTimeFormatter.ISO_DATE_TIME),
                137L);
        assertTrue("list has an element", list.size() == 1);
        assertEquals("found topic has particular id", Long.valueOf(10L), list.get(0).getId());

Запрос по HTTP будет иметь вид:

.../topics/search/findByResolutionDateBetweenAndAuthorId?resolutionDateLower=2017-01-23T00%3A00%3A00.000Z&resolutionDateUpper=2017-01-23T23%3A59%3A59.999Z&authorId=137
(не забываем про urlEncode для даты/времени)

Несколько слов по поводу сравнения объектов ZonedDateTime. Разберем такой код:

import java.time.temporal.ChronoUnit;
...
        String dateInString1 = "2016-10-06T10:40:21+03:00";
        String dateInString2 = "2016-10-06T12:40:21+05:00";
 
        String dateInString3 = "2016-10-06T07:40:21Z";
 
        ZonedDateTime zdt1 = ZonedDateTime
                .parse(dateInString1, DateTimeFormatter.ISO_DATE_TIME);
        ZonedDateTime zdt2 = ZonedDateTime
                .parse(dateInString2, DateTimeFormatter.ISO_DATE_TIME);
        ZonedDateTime zdt3 = ZonedDateTime
                .parse(dateInString3, DateTimeFormatter.ISO_DATE_TIME);
 
        System.out.println(zdt1);
        System.out.println(zdt2);
        System.out.println(zdt3);
 
        System.out.println("=========================");
 
        System.out.println("result of comparison 1: " + zdt1.compareTo(zdt2));
        System.out.println("result of comparison 2: " + zdt3.compareTo(zdt1));
        Comparator<ZonedDateTime> comparator = Comparator.comparing(
                zdt -> zdt.truncatedTo(ChronoUnit.SECONDS));
        System.out.println("result of comparison 3: " + comparator.compare(zdt1, zdt2));
        System.out.println("result of comparison 4: " + comparator.compare(zdt3, zdt1));
 
        long seconds = ChronoUnit.SECONDS.between(zdt1, zdt2);
        System.out.println("difference 1: " + seconds);
        seconds = ChronoUnit.SECONDS.between(zdt3, zdt2);
        System.out.println("difference 2: " + seconds);
 
        System.out.println("=========================");
        ZoneId zone = ZoneId.of("Asia/Muscat");
        zdt1 = ZonedDateTime
                .parse(dateInString1, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
        zdt2 = ZonedDateTime
                .parse(dateInString2, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
        zdt3 = ZonedDateTime
                .parse(dateInString3, DateTimeFormatter.ISO_DATE_TIME).withZoneSameInstant(zone);
 
        System.out.println(zdt1);
        System.out.println(zdt2);
        System.out.println(zdt3);
 
        System.out.println("result of comparison 5: " + comparator.compare(zdt1, zdt2));
        System.out.println("result of comparison 6: " + comparator.compare(zdt3, zdt2));
 
        System.out.println("result of comparison 7: " + zdt1.compareTo(zdt2));
        System.out.println("result of comparison 8: " + zdt3.compareTo(zdt2));

Как видно, мы работаем с тремя объектами, которые представляют собой один и тот же момент времени в разных часовых поясах. Код дает такой вывод:

2016-10-06T10:40:21+03:00
2016-10-06T12:40:21+05:00
2016-10-06T07:40:21Z
=========================
result of comparison 1: -1
result of comparison 2: -1
result of comparison 3: -1
result of comparison 4: -1
difference 1: 0
difference 2: 0
=========================
2016-10-06T11:40:21+04:00[Asia/Muscat]
2016-10-06T11:40:21+04:00[Asia/Muscat]
2016-10-06T11:40:21+04:00[Asia/Muscat]
result of comparison 5: 0
result of comparison 6: 0
result of comparison 7: 0
result of comparison 8: 0

Сравнения с помощью компаратора или ZonedDateTime одинаковых моментов времени в разных поясах не дают 0 (признака равенства). Надежно сравнить на равенство можно только с помощью ChronoUnit.

Напоследок приведу пример преобразования между ZonedDateTime и java.util.Date (получение даты в заданном часовом поясе):

 
    private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
        ZonedDateTime now = zdt.withZoneSameInstant(zoneId);
        String formattedZdt = now.format(dtf);
 
        Date date = null;
        try {
            date = sdf.parse(formattedZdt);
        } catch (ParseException e) {
            e.printStackTrace();
        }

А так можно вычислить границы суток в определенном часовом поясе:

        // Date day - календарный день, границы которого надо расчитать в зоне zoneId
        ZonedDateTime zdt = ZonedDateTime.ofInstant(day.toInstant(), ZoneId.systemDefault());
 
        ZonedDateTime zdtTodayStart = zdt.toLocalDate().atStartOfDay(zoneId);
        ZonedDateTime zdtTodayEnd = zdtTodayStart.plusDays(1).minusNanos(1l);
 
        String lowerBound = zdtTodayStart.format(DateTimeFormatter.ISO_DATE_TIME);
        String upperBound = zdtTodayEnd.format(DateTimeFormatter.ISO_DATE_TIME);
You can leave a response, or trackback from your own site.

Leave a Reply