Skip to content

Commit 3a21a43

Browse files
committed
Do not convert from LocalDateTime to Timestamp by default.
Most supported databases don't need that conversion. Db2 does need it and gets it through JdbcDb2Dialect. As part of the effort it became obvious that the filtering for conversions between Date and JSR310 data types was broken. It is fixed now, which required a dedicated reading conversion from LocalDateTime to Date for JdbcMySqlDialect. Closes #974
1 parent 6bd959f commit 3a21a43

File tree

9 files changed

+153
-17
lines changed

9 files changed

+153
-17
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.jdbc.core.convert;
1717

1818
import java.sql.Timestamp;
19+
import java.time.LocalDateTime;
1920
import java.time.OffsetDateTime;
2021
import java.time.ZonedDateTime;
2122
import java.time.temporal.Temporal;
@@ -54,6 +55,7 @@ public Class<?> resolvePrimitiveType(Class<?> type) {
5455
javaToDbType.put(Enum.class, String.class);
5556
javaToDbType.put(ZonedDateTime.class, String.class);
5657
javaToDbType.put(OffsetDateTime.class, OffsetDateTime.class);
58+
javaToDbType.put(LocalDateTime.class, LocalDateTime.class);
5759
javaToDbType.put(Temporal.class, Timestamp.class);
5860
}
5961

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

+12-7
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
*/
1616
package org.springframework.data.jdbc.core.convert;
1717

18-
import java.util.Arrays;
1918
import java.util.Collection;
2019
import java.util.Collections;
2120
import java.util.List;
2221
import java.util.function.Predicate;
2322

23+
import org.springframework.core.convert.converter.GenericConverter;
2424
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
2525
import org.springframework.data.convert.CustomConversions;
2626
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
@@ -38,10 +38,13 @@
3838
*/
3939
public class JdbcCustomConversions extends CustomConversions {
4040

41-
private static final Collection<Object> STORE_CONVERTERS = Collections.unmodifiableCollection(Jsr310TimestampBasedConverters.getConvertersToRegister());
41+
private static final Collection<Object> STORE_CONVERTERS = Collections
42+
.unmodifiableCollection(Jsr310TimestampBasedConverters.getConvertersToRegister());
4243
private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER,
4344
STORE_CONVERTERS);
4445

46+
private static final Predicate<ConvertiblePair> excludeConversionsBetweenDateAndJsr310Types= cp -> !isDateTimeApiConversion(cp);
47+
4548
/**
4649
* Creates an empty {@link JdbcCustomConversions} object.
4750
*/
@@ -50,21 +53,23 @@ public JdbcCustomConversions() {
5053
}
5154

5255
/**
53-
* Create a new {@link JdbcCustomConversions} instance registering the given converters and the default store converters.
56+
* Create a new {@link JdbcCustomConversions} instance registering the given converters and the default store
57+
* converters.
5458
*
5559
* @param converters must not be {@literal null}.
5660
*/
5761
public JdbcCustomConversions(List<?> converters) {
58-
super(new ConverterConfiguration(STORE_CONVERSIONS, converters, JdbcCustomConversions::isDateTimeApiConversion));
62+
super(new ConverterConfiguration(STORE_CONVERSIONS, converters, excludeConversionsBetweenDateAndJsr310Types));
5963
}
6064

6165
/**
62-
* Create a new {@link JdbcCustomConversions} instance registering the given converters and the default store converters.
66+
* Create a new {@link JdbcCustomConversions} instance registering the given converters and the default store
67+
* converters.
6368
*
6469
* @since 2.3
6570
*/
6671
public JdbcCustomConversions(StoreConversions storeConversions, List<?> userConverters) {
67-
super(new ConverterConfiguration(storeConversions, userConverters, JdbcCustomConversions::isDateTimeApiConversion));
72+
super(new ConverterConfiguration(storeConversions, userConverters, cp -> !isDateTimeApiConversion(cp)));
6873
}
6974

7075
/**
@@ -98,6 +103,6 @@ private static boolean isDateTimeApiConversion(ConvertiblePair cp) {
98103
return cp.getSourceType().getTypeName().startsWith("java.time.");
99104
}
100105

101-
return true;
106+
return false;
102107
}
103108
}

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Jsr310TimestampBasedConverters.java

+6-7
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,21 @@
4545
* @author Jens Schauder
4646
* @since 2.2
4747
*/
48-
abstract class Jsr310TimestampBasedConverters {
49-
50-
private static final List<Class<?>> CLASSES = Arrays.asList(LocalDateTime.class, LocalDate.class, LocalTime.class,
51-
Instant.class, ZoneId.class, Duration.class, Period.class);
48+
public abstract class Jsr310TimestampBasedConverters {
5249

5350
/**
54-
* Returns the converters to be registered. Will only return converters in case we're running on Java 8.
51+
* Returns the converters to be registered.
52+
*
53+
* Note that the {@link LocalDateTimeToTimestampConverter} is not included, since many database don't need that conversion.
54+
* Databases that do need it, should include it in the conversions offered by their respective dialect.
5555
*
56-
* @return
56+
* @return a collection of converters. Guaranteed to be not {@literal null}.
5757
*/
5858
public static Collection<Converter<?, ?>> getConvertersToRegister() {
5959

6060
List<Converter<?, ?>> converters = new ArrayList<>(8);
6161

6262
converters.add(TimestampToLocalDateTimeConverter.INSTANCE);
63-
converters.add(LocalDateTimeToTimestampConverter.INSTANCE);
6463
converters.add(TimestampToLocalDateConverter.INSTANCE);
6564
converters.add(LocalDateToTimestampConverter.INSTANCE);
6665
converters.add(TimestampToLocalTimeConverter.INSTANCE);

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.springframework.core.convert.converter.Converter;
2525
import org.springframework.data.convert.WritingConverter;
26+
import org.springframework.data.jdbc.core.convert.Jsr310TimestampBasedConverters;
2627
import org.springframework.data.relational.core.dialect.Db2Dialect;
2728

2829
/**
@@ -43,6 +44,7 @@ public Collection<Object> getConverters() {
4344

4445
List<Object> converters = new ArrayList<>(super.getConverters());
4546
converters.add(OffsetDateTimeToTimestampConverter.INSTANCE);
47+
converters.add(Jsr310TimestampBasedConverters.LocalDateTimeToTimestampConverter.INSTANCE);
4648

4749
return converters;
4850
}

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcMySqlDialect.java

+19
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,23 @@
1515
*/
1616
package org.springframework.data.jdbc.core.dialect;
1717

18+
import static java.time.ZoneId.*;
19+
1820
import java.sql.JDBCType;
21+
import java.time.LocalDateTime;
1922
import java.time.OffsetDateTime;
2023
import java.util.ArrayList;
2124
import java.util.Collection;
25+
import java.util.Date;
2226

2327
import org.springframework.core.convert.converter.Converter;
28+
import org.springframework.data.convert.ReadingConverter;
2429
import org.springframework.data.convert.WritingConverter;
2530
import org.springframework.data.jdbc.core.convert.JdbcValue;
2631
import org.springframework.data.relational.core.dialect.Db2Dialect;
2732
import org.springframework.data.relational.core.dialect.MySqlDialect;
2833
import org.springframework.data.relational.core.sql.IdentifierProcessing;
34+
import org.springframework.lang.NonNull;
2935

3036
/**
3137
* {@link Db2Dialect} that registers JDBC specific converters.
@@ -47,6 +53,7 @@ public Collection<Object> getConverters() {
4753

4854
ArrayList<Object> converters = new ArrayList<>(super.getConverters());
4955
converters.add(OffsetDateTimeToTimestampJdbcValueConverter.INSTANCE);
56+
converters.add(LocalDateTimeToDateConverter.INSTANCE);
5057

5158
return converters;
5259
}
@@ -61,4 +68,16 @@ public JdbcValue convert(OffsetDateTime source) {
6168
return JdbcValue.of(source, JDBCType.TIMESTAMP);
6269
}
6370
}
71+
72+
@ReadingConverter
73+
enum LocalDateTimeToDateConverter implements Converter<LocalDateTime, Date> {
74+
75+
INSTANCE;
76+
77+
@NonNull
78+
@Override
79+
public Date convert(LocalDateTime source) {
80+
return Date.from(source.atZone(systemDefault()).toInstant());
81+
}
82+
}
6483
}

Diff for: spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.time.LocalDate;
2727
import java.time.LocalDateTime;
2828
import java.time.LocalTime;
29+
import java.time.OffsetDateTime;
2930
import java.time.ZoneOffset;
3031
import java.time.ZonedDateTime;
3132
import java.util.Date;
@@ -69,12 +70,14 @@ public void testTargetTypesForPropertyType() {
6970
SoftAssertions softly = new SoftAssertions();
7071

7172
checkTargetType(softly, entity, "someEnum", String.class);
72-
checkTargetType(softly, entity, "localDateTime", Timestamp.class);
73+
checkTargetType(softly, entity, "localDateTime", LocalDateTime.class);
7374
checkTargetType(softly, entity, "localDate", Timestamp.class);
7475
checkTargetType(softly, entity, "localTime", Timestamp.class);
76+
checkTargetType(softly, entity, "zonedDateTime", String.class);
77+
checkTargetType(softly, entity, "offsetDateTime", OffsetDateTime.class);
7578
checkTargetType(softly, entity, "instant", Timestamp.class);
7679
checkTargetType(softly, entity, "date", Date.class);
77-
checkTargetType(softly, entity, "zonedDateTime", String.class);
80+
checkTargetType(softly, entity, "timestamp", Timestamp.class);
7881
checkTargetType(softly, entity, "uuid", UUID.class);
7982

8083
softly.assertAll();
@@ -165,9 +168,11 @@ private static class DummyEntity {
165168
private final LocalDateTime localDateTime;
166169
private final LocalDate localDate;
167170
private final LocalTime localTime;
171+
private final ZonedDateTime zonedDateTime;
172+
private final OffsetDateTime offsetDateTime;
168173
private final Instant instant;
169174
private final Date date;
170-
private final ZonedDateTime zonedDateTime;
175+
private final Timestamp timestamp;
171176
private final AggregateReference<DummyEntity, Long> reference;
172177
private final UUID uuid;
173178

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2021 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.dialect;
17+
18+
import java.sql.Timestamp;
19+
import java.time.LocalDateTime;
20+
import java.time.OffsetDateTime;
21+
import java.util.List;
22+
23+
import org.assertj.core.api.SoftAssertions;
24+
import org.junit.jupiter.api.Test;
25+
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
26+
27+
/**
28+
* Tests for {@link JdbcMySqlDialect}.
29+
*
30+
* @author Jens Schauder
31+
*/
32+
class JdbcDb2DialectUnitTests {
33+
34+
@Test // #974
35+
void testCustomConversions() {
36+
37+
JdbcCustomConversions customConversions = new JdbcCustomConversions(
38+
(List<?>) JdbcDb2Dialect.INSTANCE.getConverters());
39+
40+
SoftAssertions.assertSoftly(softly -> {
41+
softly.assertThat(customConversions.getCustomWriteTarget(LocalDateTime.class)).contains(Timestamp.class);
42+
softly.assertThat(customConversions.getCustomWriteTarget(OffsetDateTime.class)).contains(Timestamp.class);
43+
});
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2021 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.dialect;
17+
18+
import java.time.LocalDateTime;
19+
import java.time.OffsetDateTime;
20+
import java.util.List;
21+
22+
import org.assertj.core.api.SoftAssertions;
23+
import org.junit.jupiter.api.Test;
24+
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
25+
import org.springframework.data.jdbc.core.convert.JdbcValue;
26+
27+
/**
28+
* Tests for {@link JdbcMySqlDialect}.
29+
*
30+
* @author Jens Schauder
31+
*/
32+
class JdbcMySqlDialectUnitTests {
33+
34+
@Test // #974
35+
void testCustomConversions() {
36+
37+
JdbcCustomConversions customConversions = new JdbcCustomConversions(
38+
(List<?>) new JdbcMySqlDialect().getConverters());
39+
40+
SoftAssertions.assertSoftly(softly -> {
41+
42+
softly.assertThat(customConversions.getCustomWriteTarget(LocalDateTime.class)).isEmpty();
43+
softly.assertThat(customConversions.getCustomWriteTarget(OffsetDateTime.class)).contains(JdbcValue.class);
44+
});
45+
}
46+
}

Diff for: spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

+13
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.io.IOException;
2828
import java.sql.ResultSet;
2929
import java.time.Instant;
30+
import java.time.LocalDateTime;
3031
import java.time.OffsetDateTime;
3132
import java.time.ZoneOffset;
3233
import java.util.ArrayList;
@@ -49,6 +50,7 @@
4950
import org.springframework.data.domain.PageRequest;
5051
import org.springframework.data.domain.Pageable;
5152
import org.springframework.data.domain.Slice;
53+
import org.springframework.data.jdbc.repository.query.Modifying;
5254
import org.springframework.data.jdbc.repository.query.Query;
5355
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
5456
import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener;
@@ -492,6 +494,13 @@ public void pageQueryProjectionShouldReturnProjectedEntities() {
492494
assertThat(result.getContent().get(0).getName()).isEqualTo("Entity Name");
493495
}
494496

497+
@Test // #974
498+
@EnabledOnFeature(TestDatabaseFeatures.Feature.IS_POSTGRES)
499+
void intervalCalculation() {
500+
501+
repository.updateWithIntervalCalculation(23L, LocalDateTime.now());
502+
}
503+
495504
private Instant createDummyBeforeAndAfterNow() {
496505

497506
Instant now = Instant.now();
@@ -557,6 +566,10 @@ interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
557566

558567
@Query("SELECT * FROM DUMMY_ENTITY WHERE OFFSET_DATE_TIME > :threshhold")
559568
List<DummyEntity> findByOffsetDateTime(@Param("threshhold") OffsetDateTime threshhold);
569+
570+
@Modifying
571+
@Query("UPDATE dummy_entity SET point_in_time = :start - interval '30 minutes' WHERE id_prop = :id")
572+
void updateWithIntervalCalculation(@Param("id") Long id, @Param("start") LocalDateTime start);
560573
}
561574

562575
@Configuration

0 commit comments

Comments
 (0)