DATAJDBC-637 - Time conversion now preserve nanosecond precision.
The standard JSR 310 converters are no longer used for conversions between java.util.Date and java.time.*. New converters based converting to/from Timestamp are used. This preserves the precision because both the java.time.* API and Timestamp have nanosecond precision, while java.util.Date has not. Original pull request: #254.
This commit is contained in:
@@ -84,7 +84,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc
|
||||
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
|
||||
RelationResolver relationResolver) {
|
||||
|
||||
super(context);
|
||||
super(context, new JdbcCustomConversions());
|
||||
|
||||
Assert.notNull(relationResolver, "RelationResolver must not be null");
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.Date;
|
||||
@@ -51,7 +52,7 @@ public enum JdbcColumnTypes {
|
||||
|
||||
javaToDbType.put(Enum.class, String.class);
|
||||
javaToDbType.put(ZonedDateTime.class, String.class);
|
||||
javaToDbType.put(Temporal.class, Date.class);
|
||||
javaToDbType.put(Temporal.class, Timestamp.class);
|
||||
}
|
||||
|
||||
public abstract Class<?> resolvePrimitiveType(Class<?> type);
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
|
||||
|
||||
/**
|
||||
@@ -25,20 +27,16 @@ import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
|
||||
* {@link org.springframework.data.mapping.model.SimpleTypeHolder}
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see org.springframework.data.convert.CustomConversions
|
||||
* @see CustomConversions
|
||||
* @see org.springframework.data.mapping.model.SimpleTypeHolder
|
||||
* @see JdbcSimpleTypes
|
||||
*/
|
||||
public class JdbcCustomConversions extends org.springframework.data.convert.CustomConversions {
|
||||
public class JdbcCustomConversions extends CustomConversions {
|
||||
|
||||
private static final StoreConversions STORE_CONVERSIONS;
|
||||
private static final List<Object> STORE_CONVERTERS;
|
||||
|
||||
static {
|
||||
|
||||
STORE_CONVERTERS = Collections.emptyList();
|
||||
STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER, STORE_CONVERTERS);
|
||||
}
|
||||
private static final List<Object> STORE_CONVERTERS = Arrays
|
||||
.asList(Jsr310TimestampBasedConverters.getConvertersToRegister().toArray());
|
||||
private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER,
|
||||
STORE_CONVERTERS);
|
||||
|
||||
/**
|
||||
* Creates an empty {@link JdbcCustomConversions} object.
|
||||
@@ -53,7 +51,15 @@ public class JdbcCustomConversions extends org.springframework.data.convert.Cust
|
||||
* @param converters must not be {@literal null}.
|
||||
*/
|
||||
public JdbcCustomConversions(List<?> converters) {
|
||||
super(STORE_CONVERSIONS, converters);
|
||||
super(new ConverterConfiguration(STORE_CONVERSIONS, converters, JdbcCustomConversions::isDateTimeApiConversion));
|
||||
}
|
||||
|
||||
private static boolean isDateTimeApiConversion(
|
||||
org.springframework.core.convert.converter.GenericConverter.ConvertiblePair cp) {
|
||||
|
||||
return (cp.getSourceType().getTypeName().equals("java.util.Date")
|
||||
&& cp.getTargetType().getTypeName().startsWith("java.time.") //
|
||||
) || (cp.getTargetType().getTypeName().equals("java.util.Date")
|
||||
&& cp.getSourceType().getTypeName().startsWith("java.time."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import static java.time.Instant.*;
|
||||
import static java.time.LocalDateTime.*;
|
||||
import static java.time.ZoneId.*;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.Period;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
/**
|
||||
* Helper class to register JSR-310 specific {@link Converter} implementations. These converters are based on
|
||||
* {@link java.sql.Timestamp} instead of {@link Date} and therefore preserve nanosecond precision
|
||||
*
|
||||
* @see org.springframework.data.convert.Jsr310Converters
|
||||
* @author Jens Schauder
|
||||
* @since 2.2
|
||||
*/
|
||||
public abstract class Jsr310TimestampBasedConverters {
|
||||
|
||||
private static final List<Class<?>> CLASSES = Arrays.asList(LocalDateTime.class, LocalDate.class, LocalTime.class,
|
||||
Instant.class, ZoneId.class, Duration.class, Period.class);
|
||||
|
||||
/**
|
||||
* Returns the converters to be registered. Will only return converters in case we're running on Java 8.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static Collection<Converter<?, ?>> getConvertersToRegister() {
|
||||
|
||||
List<Converter<?, ?>> converters = new ArrayList<>();
|
||||
converters.add(TimestampToLocalDateTimeConverter.INSTANCE);
|
||||
converters.add(LocalDateTimeToTimestampConverter.INSTANCE);
|
||||
converters.add(TimestampToLocalDateConverter.INSTANCE);
|
||||
converters.add(LocalDateToTimestampConverter.INSTANCE);
|
||||
converters.add(TimestampToLocalTimeConverter.INSTANCE);
|
||||
converters.add(LocalTimeToTimestampConverter.INSTANCE);
|
||||
converters.add(TimestampToInstantConverter.INSTANCE);
|
||||
converters.add(InstantToTimestampConverter.INSTANCE);
|
||||
|
||||
return converters;
|
||||
}
|
||||
|
||||
public static boolean supports(Class<?> type) {
|
||||
|
||||
return CLASSES.contains(type);
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
public enum TimestampToLocalDateTimeConverter implements Converter<Timestamp, LocalDateTime> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LocalDateTime convert(Timestamp source) {
|
||||
return ofInstant(source.toInstant(), systemDefault());
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
public enum LocalDateTimeToTimestampConverter implements Converter<LocalDateTime, Timestamp> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Timestamp convert(LocalDateTime source) {
|
||||
return Timestamp.from(source.atZone(systemDefault()).toInstant());
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
public enum TimestampToLocalDateConverter implements Converter<Timestamp, LocalDate> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LocalDate convert(Timestamp source) {
|
||||
return source.toLocalDateTime().toLocalDate();
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
public enum LocalDateToTimestampConverter implements Converter<LocalDate, Timestamp> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Timestamp convert(LocalDate source) {
|
||||
return Timestamp.from(source.atStartOfDay(systemDefault()).toInstant());
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
public enum TimestampToLocalTimeConverter implements Converter<Timestamp, LocalTime> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LocalTime convert(Timestamp source) {
|
||||
return source.toLocalDateTime().toLocalTime();
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
public enum LocalTimeToTimestampConverter implements Converter<LocalTime, Timestamp> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Timestamp convert(LocalTime source) {
|
||||
return Timestamp.from(source.atDate(LocalDate.now()).atZone(systemDefault()).toInstant());
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
public enum TimestampToInstantConverter implements Converter<Timestamp, Instant> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Instant convert(Timestamp source) {
|
||||
return source.toInstant();
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
public enum InstantToTimestampConverter implements Converter<Instant, Timestamp> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Timestamp convert(Instant source) {
|
||||
return Timestamp.from(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -34,6 +35,7 @@ import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.Assume;
|
||||
import org.junit.ClassRule;
|
||||
@@ -850,6 +852,21 @@ public class JdbcAggregateTemplateIntegrationTests {
|
||||
template.save(saved);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-637
|
||||
public void saveAndLoadDateTimeWithFullPrecision() {
|
||||
|
||||
WithLocalDateTime entity = new WithLocalDateTime();
|
||||
entity.id = 23L;
|
||||
entity.testTime = LocalDateTime.of(5, 5, 5, 5, 5, 5, 123456789);
|
||||
|
||||
template.insert(entity);
|
||||
|
||||
WithLocalDateTime loaded = template.findById(23L, WithLocalDateTime.class);
|
||||
|
||||
assertThat(loaded.testTime).isEqualTo(entity.testTime);
|
||||
}
|
||||
|
||||
|
||||
private <T extends Number> void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate,
|
||||
Function<Number, T> toConcreteNumber) {
|
||||
saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0);
|
||||
@@ -1196,6 +1213,14 @@ public class JdbcAggregateTemplateIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Table
|
||||
static class WithLocalDateTime{
|
||||
|
||||
@Id
|
||||
Long id;
|
||||
LocalDateTime testTime;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(TestConfiguration.class)
|
||||
static class Config {
|
||||
|
||||
@@ -19,7 +19,12 @@ import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -27,13 +32,12 @@ import java.util.UUID;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.core.mapping.AggregateReference;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.util.ClassTypeInformation;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link BasicJdbcConverter}.
|
||||
@@ -47,27 +51,6 @@ public class BasicJdbcConverterUnitTests {
|
||||
throw new UnsupportedOperationException();
|
||||
});
|
||||
|
||||
@Test // DATAJDBC-104
|
||||
public void enumGetsStoredAsString() {
|
||||
|
||||
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(DummyEntity.class);
|
||||
|
||||
entity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) p -> {
|
||||
switch (p.getName()) {
|
||||
case "someEnum":
|
||||
assertThat(converter.getColumnType(p)).isEqualTo(String.class);
|
||||
break;
|
||||
case "localDateTime":
|
||||
assertThat(converter.getColumnType(p)).isEqualTo(Date.class);
|
||||
break;
|
||||
case "zonedDateTime":
|
||||
assertThat(converter.getColumnType(p)).isEqualTo(String.class);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-104, DATAJDBC-1384
|
||||
public void testTargetTypesForPropertyType() {
|
||||
|
||||
@@ -76,7 +59,11 @@ public class BasicJdbcConverterUnitTests {
|
||||
SoftAssertions softly = new SoftAssertions();
|
||||
|
||||
checkTargetType(softly, entity, "someEnum", String.class);
|
||||
checkTargetType(softly, entity, "localDateTime", Date.class);
|
||||
checkTargetType(softly, entity, "localDateTime", Timestamp.class);
|
||||
checkTargetType(softly, entity, "localDate", Timestamp.class);
|
||||
checkTargetType(softly, entity, "localTime", Timestamp.class);
|
||||
checkTargetType(softly, entity, "instant", Timestamp.class);
|
||||
checkTargetType(softly, entity, "date", Date.class);
|
||||
checkTargetType(softly, entity, "zonedDateTime", String.class);
|
||||
checkTargetType(softly, entity, "uuid", UUID.class);
|
||||
|
||||
@@ -114,6 +101,32 @@ public class BasicJdbcConverterUnitTests {
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-637
|
||||
void conversionOfDateLikeValueAndBackYieldsOriginalValue() {
|
||||
|
||||
RelationalPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
LocalDateTime testLocalDateTime = LocalDateTime.of(2001, 2, 3, 4, 5, 6, 123456789);
|
||||
checkConversionToTimestampAndBack(softly, persistentEntity, "localDateTime", testLocalDateTime);
|
||||
checkConversionToTimestampAndBack(softly, persistentEntity, "localDate", LocalDate.of(2001, 2, 3));
|
||||
checkConversionToTimestampAndBack(softly, persistentEntity, "localTime", LocalTime.of(1, 2, 3,123456789));
|
||||
checkConversionToTimestampAndBack(softly, persistentEntity, "instant", testLocalDateTime.toInstant(ZoneOffset.UTC));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void checkConversionToTimestampAndBack(SoftAssertions softly, RelationalPersistentEntity<?> persistentEntity, String propertyName,
|
||||
Object value) {
|
||||
|
||||
RelationalPersistentProperty property = persistentEntity.getRequiredPersistentProperty(propertyName);
|
||||
|
||||
Object converted = converter.writeValue(value, ClassTypeInformation.from(converter.getColumnType(property)));
|
||||
Object convertedBack = converter.readValue(converted, property.getTypeInformation());
|
||||
|
||||
softly.assertThat(convertedBack).describedAs(propertyName).isEqualTo(value);
|
||||
}
|
||||
|
||||
private void checkTargetType(SoftAssertions softly, RelationalPersistentEntity<?> persistentEntity,
|
||||
String propertyName, Class<?> expected) {
|
||||
|
||||
@@ -129,6 +142,10 @@ public class BasicJdbcConverterUnitTests {
|
||||
@Id private final Long id;
|
||||
private final SomeEnum someEnum;
|
||||
private final LocalDateTime localDateTime;
|
||||
private final LocalDate localDate;
|
||||
private final LocalTime localTime;
|
||||
private final Instant instant;
|
||||
private final Date date;
|
||||
private final ZonedDateTime zonedDateTime;
|
||||
private final AggregateReference<DummyEntity, Long> reference;
|
||||
private final UUID uuid;
|
||||
|
||||
@@ -303,3 +303,10 @@ CREATE TABLE WITH_READ_ONLY
|
||||
NAME VARCHAR(200),
|
||||
READ_ONLY VARCHAR(200) DEFAULT 'from-db'
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE WITH_LOCAL_DATE_TIME
|
||||
(
|
||||
ID PRIMARY KEY,
|
||||
TEST_TIME TIMESTAMP(9) WITHOUT TIME ZONE
|
||||
);
|
||||
@@ -305,3 +305,10 @@ CREATE TABLE VERSIONED_AGGREGATE
|
||||
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
|
||||
VERSION BIGINT
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE WITH_LOCAL_DATE_TIME
|
||||
(
|
||||
ID BIGINT PRIMARY KEY,
|
||||
TEST_TIME TIMESTAMP(9)
|
||||
);
|
||||
Reference in New Issue
Block a user