From 2efcda2a911f27599b7d9c21bf510816d8c2fa57 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 1 Dec 2020 11:18:41 +0100 Subject: [PATCH 1/4] 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. --- .../jdbc/core/convert/BasicJdbcConverter.java | 2 +- .../jdbc/core/convert/JdbcColumnTypes.java | 3 +- .../core/convert/JdbcCustomConversions.java | 28 +-- .../Jsr310TimestampBasedConverters.java | 174 ++++++++++++++++++ ...JdbcAggregateTemplateIntegrationTests.java | 25 +++ .../convert/BasicJdbcConverterUnitTests.java | 65 ++++--- ...bcAggregateTemplateIntegrationTests-h2.sql | 7 + ...AggregateTemplateIntegrationTests-hsql.sql | 7 + 8 files changed, 274 insertions(+), 37 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java index 7239e3cba8..1dc6e6c62f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java @@ -93,7 +93,7 @@ public BasicJdbcConverter( MappingContext, ? extends RelationalPersistentProperty> context, RelationResolver relationResolver) { - super(context); + super(context, new JdbcCustomConversions()); Assert.notNull(relationResolver, "RelationResolver must not be null"); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java index e949ea4a79..dc0b5296ce 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java @@ -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 Class resolvePrimitiveType(Class type) { 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); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java index b821613222..cc91830cc1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java @@ -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 @@ * {@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 STORE_CONVERTERS; - - static { - - STORE_CONVERTERS = Collections.emptyList(); - STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER, STORE_CONVERTERS); - } + private static final List 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 JdbcCustomConversions() { * @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.")); + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java new file mode 100644 index 0000000000..00d5075e7c --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java @@ -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> 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> getConvertersToRegister() { + + List> 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 { + + INSTANCE; + + @NonNull + @Override + public LocalDateTime convert(Timestamp source) { + return ofInstant(source.toInstant(), systemDefault()); + } + } + + @WritingConverter + public enum LocalDateTimeToTimestampConverter implements Converter { + + INSTANCE; + + @NonNull + @Override + public Timestamp convert(LocalDateTime source) { + return Timestamp.from(source.atZone(systemDefault()).toInstant()); + } + } + + @ReadingConverter + public enum TimestampToLocalDateConverter implements Converter { + + INSTANCE; + + @NonNull + @Override + public LocalDate convert(Timestamp source) { + return source.toLocalDateTime().toLocalDate(); + } + } + + @WritingConverter + public enum LocalDateToTimestampConverter implements Converter { + + INSTANCE; + + @NonNull + @Override + public Timestamp convert(LocalDate source) { + return Timestamp.from(source.atStartOfDay(systemDefault()).toInstant()); + } + } + + @ReadingConverter + public enum TimestampToLocalTimeConverter implements Converter { + + INSTANCE; + + @NonNull + @Override + public LocalTime convert(Timestamp source) { + return source.toLocalDateTime().toLocalTime(); + } + } + + @WritingConverter + public enum LocalTimeToTimestampConverter implements Converter { + + INSTANCE; + + @NonNull + @Override + public Timestamp convert(LocalTime source) { + return Timestamp.from(source.atDate(LocalDate.now()).atZone(systemDefault()).toInstant()); + } + } + + @ReadingConverter + public enum TimestampToInstantConverter implements Converter { + + INSTANCE; + + @NonNull + @Override + public Instant convert(Timestamp source) { + return source.toInstant(); + } + } + + @WritingConverter + public enum InstantToTimestampConverter implements Converter { + + INSTANCE; + + @NonNull + @Override + public Timestamp convert(Instant source) { + return Timestamp.from(source); + } + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java index cf41d368af..dbbb5bae1c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java @@ -25,6 +25,7 @@ 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.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 void resavingAnUnversionedEntity() { 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 void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate, Function toConcreteNumber) { saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0); @@ -1166,6 +1183,14 @@ void setVersion(Number newVersion) { } } + @Table + static class WithLocalDateTime{ + + @Id + Long id; + LocalDateTime testTime; + } + @Configuration @Import(TestConfiguration.class) static class Config { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java index 69a31f6746..b7aa39494b 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java @@ -19,7 +19,12 @@ 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 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 { throw new UnsupportedOperationException(); }); - @Test // DATAJDBC-104 - public void enumGetsStoredAsString() { - - RelationalPersistentEntity entity = context.getRequiredPersistentEntity(DummyEntity.class); - - entity.doWithProperties((PropertyHandler) 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 void testTargetTypesForPropertyType() { 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 void referencesAreNotEntitiesAndGetStoredAsTheirId() { 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 @@ private static class DummyEntity { @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 reference; private final UUID uuid; diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql index cb6dceaaac..0c1b7bdb0c 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql @@ -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 +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql index 07ee8a71f2..fda435ea4f 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql @@ -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) +); \ No newline at end of file From 869e987e55c43b1a444a83cde461983a46ed8374 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 7 Dec 2020 15:24:48 +0100 Subject: [PATCH 2/4] DATAJDBC-637 - Polishing. Make Jsr310TimestampBasedConverters package-private. Introduce convenience constructors to improve external configuration of JdbcCustomConversions. Original pull request: #254. --- .../core/convert/JdbcCustomConversions.java | 29 +++++++++++++++---- .../Jsr310TimestampBasedConverters.java | 13 +++------ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java index cc91830cc1..5f456956ed 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; +import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; @@ -27,6 +28,7 @@ * {@link org.springframework.data.mapping.model.SimpleTypeHolder} * * @author Mark Paluch + * @author Jens Schauder * @see CustomConversions * @see org.springframework.data.mapping.model.SimpleTypeHolder * @see JdbcSimpleTypes @@ -54,12 +56,27 @@ public JdbcCustomConversions(List converters) { super(new ConverterConfiguration(STORE_CONVERSIONS, converters, JdbcCustomConversions::isDateTimeApiConversion)); } - private static boolean isDateTimeApiConversion( - org.springframework.core.convert.converter.GenericConverter.ConvertiblePair cp) { + /** + * Create a new {@link JdbcCustomConversions} instance given + * {@link org.springframework.data.convert.CustomConversions.ConverterConfiguration}. + * + * @param converterConfiguration must not be {@literal null}. + * @since 2.2 + */ + public JdbcCustomConversions(ConverterConfiguration converterConfiguration) { + super(converterConfiguration); + } + + private static boolean isDateTimeApiConversion(ConvertiblePair cp) { + + if (cp.getSourceType().equals(java.util.Date.class) && cp.getTargetType().getTypeName().startsWith("java.time.")) { + return true; + } + + if (cp.getTargetType().equals(java.util.Date.class) && cp.getSourceType().getTypeName().startsWith("java.time.")) { + return true; + } - 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.")); + return false; } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java index 00d5075e7c..1b76fd4d03 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java @@ -15,7 +15,6 @@ */ package org.springframework.data.jdbc.core.convert; -import static java.time.Instant.*; import static java.time.LocalDateTime.*; import static java.time.ZoneId.*; @@ -41,12 +40,12 @@ /** * 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 { +abstract class Jsr310TimestampBasedConverters { private static final List> CLASSES = Arrays.asList(LocalDateTime.class, LocalDate.class, LocalTime.class, Instant.class, ZoneId.class, Duration.class, Period.class); @@ -58,7 +57,8 @@ public abstract class Jsr310TimestampBasedConverters { */ public static Collection> getConvertersToRegister() { - List> converters = new ArrayList<>(); + List> converters = new ArrayList<>(8); + converters.add(TimestampToLocalDateTimeConverter.INSTANCE); converters.add(LocalDateTimeToTimestampConverter.INSTANCE); converters.add(TimestampToLocalDateConverter.INSTANCE); @@ -71,11 +71,6 @@ public abstract class Jsr310TimestampBasedConverters { return converters; } - public static boolean supports(Class type) { - - return CLASSES.contains(type); - } - @ReadingConverter public enum TimestampToLocalDateTimeConverter implements Converter { From 502ccfe974ae8c774913c6e37174c4598a32d821 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Tue, 8 Dec 2020 10:26:27 -0600 Subject: [PATCH 3/4] DATAJDBC-637 - Fix typo in H2 testing. --- .../JdbcAggregateTemplateIntegrationTests-h2.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql index 0c1b7bdb0c..c37a60036d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql @@ -307,6 +307,6 @@ CREATE TABLE WITH_READ_ONLY CREATE TABLE WITH_LOCAL_DATE_TIME ( - ID PRIMARY KEY, + ID SERIAL PRIMARY KEY, TEST_TIME TIMESTAMP(9) WITHOUT TIME ZONE ); \ No newline at end of file From 8af9a3fafc5e86698d173ffbb3330977e92cbafe Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 9 Dec 2020 09:03:46 +0100 Subject: [PATCH 4/4] DATAJDBC-637 - Fix tests for all databases. Adds SQL scripts for all databases. Separates tests for all databases vs. those that actually support nanosecond precision. --- .../JdbcAggregateTemplateIntegrationTests.java | 17 ++++++++++++++++- .../data/jdbc/testing/TestDatabaseFeatures.java | 6 ++++++ ...dbcAggregateTemplateIntegrationTests-db2.sql | 8 ++++++++ ...JdbcAggregateTemplateIntegrationTests-h2.sql | 2 +- ...ggregateTemplateIntegrationTests-mariadb.sql | 7 +++++++ ...cAggregateTemplateIntegrationTests-mssql.sql | 9 +++++++++ ...cAggregateTemplateIntegrationTests-mysql.sql | 7 +++++++ ...AggregateTemplateIntegrationTests-oracle.sql | 8 ++++++++ ...gregateTemplateIntegrationTests-postgres.sql | 6 ++++++ 9 files changed, 68 insertions(+), 2 deletions(-) diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java index dbbb5bae1c..7248d14aca 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java @@ -823,11 +823,26 @@ public void resavingAnUnversionedEntity() { } @Test // DATAJDBC-637 + @EnabledOnFeature(SUPPORTS_NANOSECOND_PRECISION) public void saveAndLoadDateTimeWithFullPrecision() { WithLocalDateTime entity = new WithLocalDateTime(); entity.id = 23L; - entity.testTime = LocalDateTime.of(5, 5, 5, 5, 5, 5, 123456789); + entity.testTime = LocalDateTime.of(2005, 5, 5, 5, 5, 5, 123456789); + + template.insert(entity); + + WithLocalDateTime loaded = template.findById(23L, WithLocalDateTime.class); + + assertThat(loaded.testTime).isEqualTo(entity.testTime); + } + + @Test // DATAJDBC-637 + public void saveAndLoadDateTimeWithMicrosecondPrecision() { + + WithLocalDateTime entity = new WithLocalDateTime(); + entity.id = 23L; + entity.testTime = LocalDateTime.of(2005, 5, 5, 5, 5, 5, 123456000); template.insert(entity); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java index 20a6c2e9c5..21a02f95bb 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java @@ -72,6 +72,11 @@ private void supportsArrays() { assumeThat(database).isNotIn(Database.MySql, Database.MariaDb, Database.SqlServer, Database.Db2, Database.Oracle); } + private void supportsNanosecondPrecision() { + + assumeThat(database).isNotIn(Database.MySql, Database.PostgreSql, Database.MariaDb, Database.SqlServer); + } + private void supportsMultiDimensionalArrays() { supportsArrays(); @@ -109,6 +114,7 @@ public enum Feature { SUPPORTS_HUGE_NUMBERS(TestDatabaseFeatures::supportsHugeNumbers), // SUPPORTS_ARRAYS(TestDatabaseFeatures::supportsArrays), // SUPPORTS_GENERATED_IDS_IN_REFERENCED_ENTITIES(TestDatabaseFeatures::supportsGeneratedIdsInReferencedEntities), // + SUPPORTS_NANOSECOND_PRECISION(TestDatabaseFeatures::supportsNanosecondPrecision), // IS_HSQL(f -> f.databaseIs(Database.Hsql)); private final Consumer featureMethod; diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql index 0332be5eca..7b2b8d63e5 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql @@ -35,6 +35,7 @@ DROP TABLE NO_ID_LIST_CHAIN4; DROP TABLE WITH_READ_ONLY; DROP TABLE VERSIONED_AGGREGATE; +DROP TABLE WITH_LOCAL_DATE_TIME; CREATE TABLE LEGO_SET ( @@ -343,3 +344,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 NOT NULL PRIMARY KEY, + TEST_TIME TIMESTAMP(9) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql index c37a60036d..63294ab7d0 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql @@ -307,6 +307,6 @@ CREATE TABLE WITH_READ_ONLY CREATE TABLE WITH_LOCAL_DATE_TIME ( - ID SERIAL PRIMARY KEY, + ID BIGINT PRIMARY KEY, TEST_TIME TIMESTAMP(9) WITHOUT TIME ZONE ); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql index 31bf495f8b..31f000fc7c 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql @@ -290,3 +290,10 @@ CREATE TABLE VERSIONED_AGGREGATE ID BIGINT AUTO_INCREMENT PRIMARY KEY, VERSION BIGINT ); + + +CREATE TABLE WITH_LOCAL_DATE_TIME +( + ID BIGINT PRIMARY KEY, + TEST_TIME TIMESTAMP(6) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql index 3f37da7827..ba982ac9ec 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql @@ -316,3 +316,12 @@ CREATE TABLE VERSIONED_AGGREGATE ID BIGINT IDENTITY PRIMARY KEY, VERSION BIGINT ); + + +DROP TABLE IF EXISTS WITH_LOCAL_DATE_TIME; + +CREATE TABLE WITH_LOCAL_DATE_TIME +( + ID BIGINT PRIMARY KEY, + TEST_TIME datetime2(7) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql index 9d8ba80f35..4df794b78a 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql @@ -295,3 +295,10 @@ CREATE TABLE WITH_READ_ONLY NAME VARCHAR(200), READ_ONLY VARCHAR(200) DEFAULT 'from-db' ); + + +CREATE TABLE WITH_LOCAL_DATE_TIME +( + ID BIGINT PRIMARY KEY, + TEST_TIME TIMESTAMP(6) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql index 6559285968..b1a97093b0 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql @@ -27,6 +27,7 @@ DROP TABLE NO_ID_MAP_CHAIN3 CASCADE CONSTRAINTS PURGE; DROP TABLE NO_ID_MAP_CHAIN4 CASCADE CONSTRAINTS PURGE; DROP TABLE VERSIONED_AGGREGATE CASCADE CONSTRAINTS PURGE; DROP TABLE WITH_READ_ONLY CASCADE CONSTRAINTS PURGE; +DROP TABLE WITH_LOCAL_DATE_TIME CASCADE CONSTRAINTS PURGE; CREATE TABLE LEGO_SET ( @@ -325,3 +326,10 @@ CREATE TABLE WITH_READ_ONLY NAME VARCHAR(200), READ_ONLY VARCHAR(200) DEFAULT 'from-db' ); + + +CREATE TABLE WITH_LOCAL_DATE_TIME +( + ID NUMBER PRIMARY KEY, + TEST_TIME TIMESTAMP(9) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql index 30b158fc3d..47c6841e6e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql @@ -318,3 +318,9 @@ CREATE TABLE WITH_READ_ONLY NAME VARCHAR(200), READ_ONLY VARCHAR(200) DEFAULT 'from-db' ); + +CREATE TABLE WITH_LOCAL_DATE_TIME +( + ID BIGINT PRIMARY KEY, + TEST_TIME TIMESTAMP(9) WITHOUT TIME ZONE +); \ No newline at end of file