Browse Source

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.
pull/1035/head
Jens Schauder 5 years ago
parent
commit
69fe41dd55
No known key found for this signature in database
GPG Key ID: 996B1389BA0721C3
  1. 2
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java
  2. 3
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java
  3. 28
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java
  4. 174
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java
  5. 25
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java
  6. 65
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java
  7. 7
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql
  8. 7
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql

2
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java

@ -93,7 +93,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc @@ -93,7 +93,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");

3
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java

@ -15,6 +15,7 @@ @@ -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 { @@ -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);

28
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java

@ -15,9 +15,11 @@ @@ -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; @@ -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 @@ -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."));
}
}

174
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java

@ -0,0 +1,174 @@ @@ -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);
}
}
}

25
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java

@ -25,6 +25,7 @@ import lombok.EqualsAndHashCode; @@ -25,6 +25,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;
@ -36,6 +37,7 @@ import java.util.Set; @@ -36,6 +37,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.jupiter.api.Test;
@ -820,6 +822,21 @@ public class JdbcAggregateTemplateIntegrationTests { @@ -820,6 +822,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);
@ -1166,6 +1183,14 @@ public class JdbcAggregateTemplateIntegrationTests { @@ -1166,6 +1183,14 @@ public class JdbcAggregateTemplateIntegrationTests {
}
}
@Table
static class WithLocalDateTime{
@Id
Long id;
LocalDateTime testTime;
}
@Configuration
@Import(TestConfiguration.class)
static class Config {

65
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java

@ -19,7 +19,12 @@ import static org.assertj.core.api.Assertions.*; @@ -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; @@ -27,13 +32,12 @@ import java.util.UUID;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;

7
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql

@ -303,3 +303,10 @@ CREATE TABLE WITH_READ_ONLY @@ -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
);

7
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql

@ -305,3 +305,10 @@ CREATE TABLE VERSIONED_AGGREGATE @@ -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)
);
Loading…
Cancel
Save