Skip to content

Commit f8fcb0d

Browse files
schaudermp911de
authored andcommitted
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.
1 parent 550bec5 commit f8fcb0d

File tree

8 files changed

+274
-37
lines changed

8 files changed

+274
-37
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public BasicJdbcConverter(
9393
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
9494
RelationResolver relationResolver) {
9595

96-
super(context);
96+
super(context, new JdbcCustomConversions());
9797

9898
Assert.notNull(relationResolver, "RelationResolver must not be null");
9999

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.jdbc.core.convert;
1717

18+
import java.sql.Timestamp;
1819
import java.time.ZonedDateTime;
1920
import java.time.temporal.Temporal;
2021
import java.util.Date;
@@ -51,7 +52,7 @@ public Class<?> resolvePrimitiveType(Class<?> type) {
5152

5253
javaToDbType.put(Enum.class, String.class);
5354
javaToDbType.put(ZonedDateTime.class, String.class);
54-
javaToDbType.put(Temporal.class, Date.class);
55+
javaToDbType.put(Temporal.class, Timestamp.class);
5556
}
5657

5758
public abstract Class<?> resolvePrimitiveType(Class<?> type);

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,28 @@
1515
*/
1616
package org.springframework.data.jdbc.core.convert;
1717

18+
import java.util.Arrays;
1819
import java.util.Collections;
1920
import java.util.List;
2021

22+
import org.springframework.data.convert.CustomConversions;
2123
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
2224

2325
/**
2426
* Value object to capture custom conversion. {@link JdbcCustomConversions} also act as factory for
2527
* {@link org.springframework.data.mapping.model.SimpleTypeHolder}
2628
*
2729
* @author Mark Paluch
28-
* @see org.springframework.data.convert.CustomConversions
30+
* @see CustomConversions
2931
* @see org.springframework.data.mapping.model.SimpleTypeHolder
3032
* @see JdbcSimpleTypes
3133
*/
32-
public class JdbcCustomConversions extends org.springframework.data.convert.CustomConversions {
34+
public class JdbcCustomConversions extends CustomConversions {
3335

34-
private static final StoreConversions STORE_CONVERSIONS;
35-
private static final List<Object> STORE_CONVERTERS;
36-
37-
static {
38-
39-
STORE_CONVERTERS = Collections.emptyList();
40-
STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER, STORE_CONVERTERS);
41-
}
36+
private static final List<Object> STORE_CONVERTERS = Arrays
37+
.asList(Jsr310TimestampBasedConverters.getConvertersToRegister().toArray());
38+
private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER,
39+
STORE_CONVERTERS);
4240

4341
/**
4442
* Creates an empty {@link JdbcCustomConversions} object.
@@ -53,7 +51,15 @@ public JdbcCustomConversions() {
5351
* @param converters must not be {@literal null}.
5452
*/
5553
public JdbcCustomConversions(List<?> converters) {
56-
super(STORE_CONVERSIONS, converters);
54+
super(new ConverterConfiguration(STORE_CONVERSIONS, converters, JdbcCustomConversions::isDateTimeApiConversion));
5755
}
5856

57+
private static boolean isDateTimeApiConversion(
58+
org.springframework.core.convert.converter.GenericConverter.ConvertiblePair cp) {
59+
60+
return (cp.getSourceType().getTypeName().equals("java.util.Date")
61+
&& cp.getTargetType().getTypeName().startsWith("java.time.") //
62+
) || (cp.getTargetType().getTypeName().equals("java.util.Date")
63+
&& cp.getSourceType().getTypeName().startsWith("java.time."));
64+
}
5965
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core.convert;
17+
18+
import static java.time.Instant.*;
19+
import static java.time.LocalDateTime.*;
20+
import static java.time.ZoneId.*;
21+
22+
import java.sql.Timestamp;
23+
import java.time.Duration;
24+
import java.time.Instant;
25+
import java.time.LocalDate;
26+
import java.time.LocalDateTime;
27+
import java.time.LocalTime;
28+
import java.time.Period;
29+
import java.time.ZoneId;
30+
import java.util.ArrayList;
31+
import java.util.Arrays;
32+
import java.util.Collection;
33+
import java.util.Date;
34+
import java.util.List;
35+
36+
import org.springframework.core.convert.converter.Converter;
37+
import org.springframework.data.convert.ReadingConverter;
38+
import org.springframework.data.convert.WritingConverter;
39+
import org.springframework.lang.NonNull;
40+
41+
/**
42+
* Helper class to register JSR-310 specific {@link Converter} implementations. These converters are based on
43+
* {@link java.sql.Timestamp} instead of {@link Date} and therefore preserve nanosecond precision
44+
*
45+
* @see org.springframework.data.convert.Jsr310Converters
46+
* @author Jens Schauder
47+
* @since 2.2
48+
*/
49+
public abstract class Jsr310TimestampBasedConverters {
50+
51+
private static final List<Class<?>> CLASSES = Arrays.asList(LocalDateTime.class, LocalDate.class, LocalTime.class,
52+
Instant.class, ZoneId.class, Duration.class, Period.class);
53+
54+
/**
55+
* Returns the converters to be registered. Will only return converters in case we're running on Java 8.
56+
*
57+
* @return
58+
*/
59+
public static Collection<Converter<?, ?>> getConvertersToRegister() {
60+
61+
List<Converter<?, ?>> converters = new ArrayList<>();
62+
converters.add(TimestampToLocalDateTimeConverter.INSTANCE);
63+
converters.add(LocalDateTimeToTimestampConverter.INSTANCE);
64+
converters.add(TimestampToLocalDateConverter.INSTANCE);
65+
converters.add(LocalDateToTimestampConverter.INSTANCE);
66+
converters.add(TimestampToLocalTimeConverter.INSTANCE);
67+
converters.add(LocalTimeToTimestampConverter.INSTANCE);
68+
converters.add(TimestampToInstantConverter.INSTANCE);
69+
converters.add(InstantToTimestampConverter.INSTANCE);
70+
71+
return converters;
72+
}
73+
74+
public static boolean supports(Class<?> type) {
75+
76+
return CLASSES.contains(type);
77+
}
78+
79+
@ReadingConverter
80+
public enum TimestampToLocalDateTimeConverter implements Converter<Timestamp, LocalDateTime> {
81+
82+
INSTANCE;
83+
84+
@NonNull
85+
@Override
86+
public LocalDateTime convert(Timestamp source) {
87+
return ofInstant(source.toInstant(), systemDefault());
88+
}
89+
}
90+
91+
@WritingConverter
92+
public enum LocalDateTimeToTimestampConverter implements Converter<LocalDateTime, Timestamp> {
93+
94+
INSTANCE;
95+
96+
@NonNull
97+
@Override
98+
public Timestamp convert(LocalDateTime source) {
99+
return Timestamp.from(source.atZone(systemDefault()).toInstant());
100+
}
101+
}
102+
103+
@ReadingConverter
104+
public enum TimestampToLocalDateConverter implements Converter<Timestamp, LocalDate> {
105+
106+
INSTANCE;
107+
108+
@NonNull
109+
@Override
110+
public LocalDate convert(Timestamp source) {
111+
return source.toLocalDateTime().toLocalDate();
112+
}
113+
}
114+
115+
@WritingConverter
116+
public enum LocalDateToTimestampConverter implements Converter<LocalDate, Timestamp> {
117+
118+
INSTANCE;
119+
120+
@NonNull
121+
@Override
122+
public Timestamp convert(LocalDate source) {
123+
return Timestamp.from(source.atStartOfDay(systemDefault()).toInstant());
124+
}
125+
}
126+
127+
@ReadingConverter
128+
public enum TimestampToLocalTimeConverter implements Converter<Timestamp, LocalTime> {
129+
130+
INSTANCE;
131+
132+
@NonNull
133+
@Override
134+
public LocalTime convert(Timestamp source) {
135+
return source.toLocalDateTime().toLocalTime();
136+
}
137+
}
138+
139+
@WritingConverter
140+
public enum LocalTimeToTimestampConverter implements Converter<LocalTime, Timestamp> {
141+
142+
INSTANCE;
143+
144+
@NonNull
145+
@Override
146+
public Timestamp convert(LocalTime source) {
147+
return Timestamp.from(source.atDate(LocalDate.now()).atZone(systemDefault()).toInstant());
148+
}
149+
}
150+
151+
@ReadingConverter
152+
public enum TimestampToInstantConverter implements Converter<Timestamp, Instant> {
153+
154+
INSTANCE;
155+
156+
@NonNull
157+
@Override
158+
public Instant convert(Timestamp source) {
159+
return source.toInstant();
160+
}
161+
}
162+
163+
@WritingConverter
164+
public enum InstantToTimestampConverter implements Converter<Instant, Timestamp> {
165+
166+
INSTANCE;
167+
168+
@NonNull
169+
@Override
170+
public Timestamp convert(Instant source) {
171+
return Timestamp.from(source);
172+
}
173+
}
174+
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import lombok.Value;
2626
import lombok.With;
2727

28+
import java.time.LocalDateTime;
2829
import java.util.ArrayList;
2930
import java.util.Arrays;
3031
import java.util.Collections;
@@ -36,6 +37,7 @@
3637
import java.util.function.Function;
3738
import java.util.stream.IntStream;
3839

40+
import net.bytebuddy.asm.Advice;
3941
import org.assertj.core.api.SoftAssertions;
4042

4143
import org.junit.jupiter.api.Test;
@@ -820,6 +822,21 @@ public void resavingAnUnversionedEntity() {
820822
template.save(saved);
821823
}
822824

825+
@Test // DATAJDBC-637
826+
public void saveAndLoadDateTimeWithFullPrecision() {
827+
828+
WithLocalDateTime entity = new WithLocalDateTime();
829+
entity.id = 23L;
830+
entity.testTime = LocalDateTime.of(5, 5, 5, 5, 5, 5, 123456789);
831+
832+
template.insert(entity);
833+
834+
WithLocalDateTime loaded = template.findById(23L, WithLocalDateTime.class);
835+
836+
assertThat(loaded.testTime).isEqualTo(entity.testTime);
837+
}
838+
839+
823840
private <T extends Number> void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate,
824841
Function<Number, T> toConcreteNumber) {
825842
saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0);
@@ -1166,6 +1183,14 @@ void setVersion(Number newVersion) {
11661183
}
11671184
}
11681185

1186+
@Table
1187+
static class WithLocalDateTime{
1188+
1189+
@Id
1190+
Long id;
1191+
LocalDateTime testTime;
1192+
}
1193+
11691194
@Configuration
11701195
@Import(TestConfiguration.class)
11711196
static class Config {

0 commit comments

Comments
 (0)