diff --git a/pom.xml b/pom.xml index d6ae6cbd4b..33a1ceec14 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.2.0-SNAPSHOT + 2.2.0-DATAJDBC-637-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index a922ef00a2..b865f71f30 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.2.0-SNAPSHOT + 2.2.0-DATAJDBC-637-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 6f04d6b4b0..2f3603262b 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.2.0-SNAPSHOT + 2.2.0-DATAJDBC-637-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.2.0-SNAPSHOT + 2.2.0-DATAJDBC-637-SNAPSHOT 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 diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 6b2507aabe..f3cef94e46 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.2.0-SNAPSHOT + 2.2.0-DATAJDBC-637-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.2.0-SNAPSHOT + 2.2.0-DATAJDBC-637-SNAPSHOT