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); |