diff --git a/pom.xml b/pom.xml index eeaa0b9e93..db8360767d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-gh-935-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 03d6a5c2a0..756e244210 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.3.0-SNAPSHOT + 2.3.0-gh-935-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index d7722eca4e..4d36016962 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.3.0-SNAPSHOT + 2.3.0-gh-935-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-gh-935-SNAPSHOT @@ -141,7 +141,7 @@ com.h2database h2 ${h2.version} - test + true @@ -190,7 +190,7 @@ com.microsoft.sqlserver mssql-jdbc ${mssql.version} - test + true 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 4ab9deaf0e..bb7a0ef3d1 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 @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core.convert; import java.sql.Timestamp; +import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.time.temporal.Temporal; import java.util.Date; @@ -52,6 +53,7 @@ public Class resolvePrimitiveType(Class type) { javaToDbType.put(Enum.class, String.class); javaToDbType.put(ZonedDateTime.class, String.class); + javaToDbType.put(OffsetDateTime.class, OffsetDateTime.class); javaToDbType.put(Temporal.class, Timestamp.class); } 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 45ce0c6bc6..97a5b3cbc7 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 @@ -16,8 +16,10 @@ package org.springframework.data.jdbc.core.convert; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; import org.springframework.data.convert.CustomConversions; @@ -29,14 +31,14 @@ * * @author Mark Paluch * @author Jens Schauder + * @author Christoph Strobl * @see CustomConversions * @see org.springframework.data.mapping.model.SimpleTypeHolder * @see JdbcSimpleTypes */ public class JdbcCustomConversions extends CustomConversions { - private static final List STORE_CONVERTERS = Arrays - .asList(Jsr310TimestampBasedConverters.getConvertersToRegister().toArray()); + private static final Collection STORE_CONVERTERS = Collections.unmodifiableCollection(Jsr310TimestampBasedConverters.getConvertersToRegister()); private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER, STORE_CONVERTERS); @@ -48,7 +50,7 @@ public JdbcCustomConversions() { } /** - * Create a new {@link JdbcCustomConversions} instance registering the given converters. + * Create a new {@link JdbcCustomConversions} instance registering the given converters and the default store converters. * * @param converters must not be {@literal null}. */ @@ -56,6 +58,15 @@ public JdbcCustomConversions(List converters) { super(new ConverterConfiguration(STORE_CONVERSIONS, converters, JdbcCustomConversions::isDateTimeApiConversion)); } + /** + * Create a new {@link JdbcCustomConversions} instance registering the given converters and the default store converters. + * + * @since 2.3 + */ + public JdbcCustomConversions(StoreConversions storeConversions, List userConverters) { + super(new ConverterConfiguration(storeConversions, userConverters, JdbcCustomConversions::isDateTimeApiConversion)); + } + /** * Create a new {@link JdbcCustomConversions} instance given * {@link org.springframework.data.convert.CustomConversions.ConverterConfiguration}. @@ -67,6 +78,16 @@ public JdbcCustomConversions(ConverterConfiguration converterConfiguration) { super(converterConfiguration); } + /** + * Obtain a read only copy of default store converters. + * + * @return never {@literal null}. + * @since 2.3 + */ + public static Collection storeConverters() { + return STORE_CONVERTERS; + } + private static boolean isDateTimeApiConversion(ConvertiblePair cp) { if (cp.getSourceType().equals(java.util.Date.class)) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java new file mode 100644 index 0000000000..d99f9cc39a --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 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.dialect; + +import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.relational.core.dialect.Db2Dialect; + +/** + * {@link Db2Dialect} that registers JDBC specific converters. + * + * @author Jens Schauder + * @author Christoph Strobl + * @since 2.3 + */ +public class JdbcDb2Dialect extends Db2Dialect { + + public static JdbcDb2Dialect INSTANCE = new JdbcDb2Dialect(); + + protected JdbcDb2Dialect() {} + + @Override + public Collection getConverters() { + + List converters = new ArrayList<>(super.getConverters()); + converters.add(OffsetDateTimeToTimestampConverter.INSTANCE); + + return converters; + } + + /** + * {@link WritingConverter} from {@link OffsetDateTime} to {@link Timestamp}. The conversion preserves the + * {@link java.time.Instant} represented by {@link OffsetDateTime} + * + * @author Jens Schauder + * @since 2.3 + */ + @WritingConverter + enum OffsetDateTimeToTimestampConverter implements Converter { + + INSTANCE; + + @Override + public Timestamp convert(OffsetDateTime source) { + return Timestamp.from(source.toInstant()); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcH2Dialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcH2Dialect.java new file mode 100644 index 0000000000..e95ae77414 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcH2Dialect.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 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.dialect; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.h2.api.TimestampWithTimeZone; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.relational.core.dialect.Db2Dialect; +import org.springframework.data.relational.core.dialect.H2Dialect; + +/** + * {@link Db2Dialect} that registers JDBC specific converters. + * + * @author Jens Schauder + * @author Christoph Strobl + * @since 2.3 + */ +public class JdbcH2Dialect extends H2Dialect { + + public static JdbcH2Dialect INSTANCE = new JdbcH2Dialect(); + + protected JdbcH2Dialect() {} + + @Override + public Collection getConverters() { + + List converters = new ArrayList<>(super.getConverters()); + converters.add(TimestampWithTimeZoneToOffsetDateTimeConverter.INSTANCE); + return converters; + } + + @ReadingConverter + enum TimestampWithTimeZoneToOffsetDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public OffsetDateTime convert(TimestampWithTimeZone source) { + + long nanosInSecond = 1_000_000_000; + long nanosInMinute = nanosInSecond * 60; + long nanosInHour = nanosInMinute * 60; + + long hours = (source.getNanosSinceMidnight() / nanosInHour); + + long nanosInHours = hours * nanosInHour; + long nanosLeft = source.getNanosSinceMidnight() - nanosInHours; + long minutes = nanosLeft / nanosInMinute; + + long nanosInMinutes = minutes * nanosInMinute; + nanosLeft -= nanosInMinutes; + long seconds = nanosLeft / nanosInSecond; + + long nanosInSeconds = seconds * nanosInSecond; + nanosLeft -= nanosInSeconds; + ZoneOffset offset = ZoneOffset.ofTotalSeconds(source.getTimeZoneOffsetSeconds()); + + return OffsetDateTime.of(source.getYear(), source.getMonth(), source.getDay(), (int) hours, (int) minutes, + (int) seconds, (int) nanosLeft, offset); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcMySqlDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcMySqlDialect.java new file mode 100644 index 0000000000..2dd8a86907 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcMySqlDialect.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 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.dialect; + +import java.sql.JDBCType; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.jdbc.core.convert.JdbcValue; +import org.springframework.data.relational.core.dialect.Db2Dialect; +import org.springframework.data.relational.core.dialect.MySqlDialect; +import org.springframework.data.relational.core.sql.IdentifierProcessing; + +/** + * {@link Db2Dialect} that registers JDBC specific converters. + * + * @author Jens Schauder + * @author Christoph Strobl + * @since 2.3 + */ +public class JdbcMySqlDialect extends MySqlDialect { + + public JdbcMySqlDialect(IdentifierProcessing identifierProcessing) { + super(identifierProcessing); + } + + protected JdbcMySqlDialect() {} + + @Override + public Collection getConverters() { + + ArrayList converters = new ArrayList<>(super.getConverters()); + converters.add(OffsetDateTimeToTimestampJdbcValueConverter.INSTANCE); + + return converters; + } + + @WritingConverter + enum OffsetDateTimeToTimestampJdbcValueConverter implements Converter { + + INSTANCE; + + @Override + public JdbcValue convert(OffsetDateTime source) { + return JdbcValue.of(source, JDBCType.TIMESTAMP); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java new file mode 100644 index 0000000000..9883c23a46 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021 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.dialect; + +import microsoft.sql.DateTimeOffset; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.relational.core.dialect.SqlServerDialect; + +/** + * {@link SqlServerDialect} that registers JDBC specific converters. + * + * @author Jens Schauder + * @author Christoph Strobl + * @since 2.3 + */ +public class JdbcSqlServerDialect extends SqlServerDialect { + + public static JdbcSqlServerDialect INSTANCE = new JdbcSqlServerDialect(); + + @Override + public Collection getConverters() { + + List converters = new ArrayList<>(super.getConverters()); + converters.add(DateTimeOffsetToOffsetDateTimeConverter.INSTANCE); + return converters; + } + + @ReadingConverter + enum DateTimeOffsetToOffsetDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public OffsetDateTime convert(DateTimeOffset source) { + return source.getOffsetDateTime(); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index fa14484187..be6398b3cf 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -15,13 +15,23 @@ */ package org.springframework.data.jdbc.repository.config; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.CustomConversions.StoreConversions; import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; @@ -32,8 +42,12 @@ import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; +import org.springframework.data.jdbc.core.dialect.JdbcDb2Dialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; +import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.conversion.RelationalConverter; +import org.springframework.data.relational.core.dialect.Db2Dialect; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -50,7 +64,11 @@ * @since 1.1 */ @Configuration(proxyBeanMethods = false) -public class AbstractJdbcConfiguration { +public class AbstractJdbcConfiguration implements ApplicationContextAware { + + private static Logger LOG = LoggerFactory.getLogger(AbstractJdbcConfiguration.class); + + private ApplicationContext applicationContext; /** * Register a {@link JdbcMappingContext} and apply an optional {@link NamingStrategy}. @@ -71,7 +89,8 @@ public JdbcMappingContext jdbcMappingContext(Optional namingStra /** * Creates a {@link RelationalConverter} using the configured - * {@link #jdbcMappingContext(Optional, JdbcCustomConversions)}. Will get {@link #jdbcCustomConversions()} applied. + * {@link #jdbcMappingContext(Optional, JdbcCustomConversions)}. Will get {@link #jdbcCustomConversions()} ()} + * applied. * * @see #jdbcMappingContext(Optional, JdbcCustomConversions) * @see #jdbcCustomConversions() @@ -84,7 +103,7 @@ public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext, NamedParam DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations()); return new BasicJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory, - dialect.getIdentifierProcessing()); + dialect.getIdentifierProcessing()); } /** @@ -97,7 +116,33 @@ public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext, NamedParam */ @Bean public JdbcCustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(); + + try { + + Dialect dialect = applicationContext.getBean(Dialect.class); + SimpleTypeHolder simpleTypeHolder = dialect.simpleTypes().isEmpty() ? JdbcSimpleTypes.HOLDER : new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER); + + return new JdbcCustomConversions( + CustomConversions.StoreConversions.of(simpleTypeHolder, storeConverters(dialect)), userConverters()); + + } catch (NoSuchBeanDefinitionException exception) { + + LOG.warn("No dialect found. CustomConversions will be configured without dialect specific conversions."); + + return new JdbcCustomConversions(); + } + } + + private List userConverters() { + return Collections.emptyList(); + } + + private List storeConverters(Dialect dialect) { + + List converters = new ArrayList<>(); + converters.addAll(dialect.getConverters()); + converters.addAll(JdbcCustomConversions.storeConverters()); + return converters; } /** @@ -134,7 +179,7 @@ public DataAccessStrategy dataAccessStrategyBean(NamedParameterJdbcOperations op * Resolves a {@link Dialect JDBC dialect} by inspecting {@link NamedParameterJdbcOperations}. * * @param operations the {@link NamedParameterJdbcOperations} allowing access to a {@link java.sql.Connection}. - * @return + * @return the {@link Dialect} to be used. * @since 2.0 * @throws org.springframework.data.jdbc.repository.config.DialectResolver.NoDialectException if the {@link Dialect} * cannot be determined. @@ -143,4 +188,9 @@ public DataAccessStrategy dataAccessStrategyBean(NamedParameterJdbcOperations op public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { return DialectResolver.getDialect(operations.getJdbcOperations()); } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java index 36cb6b714f..d7f49fd657 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java @@ -28,10 +28,15 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.dao.NonTransientDataAccessException; +import org.springframework.data.jdbc.core.dialect.JdbcDb2Dialect; +import org.springframework.data.jdbc.core.dialect.JdbcH2Dialect; +import org.springframework.data.jdbc.core.dialect.JdbcMySqlDialect; +import org.springframework.data.jdbc.core.dialect.JdbcSqlServerDialect; import org.springframework.data.relational.core.dialect.Db2Dialect; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.H2Dialect; import org.springframework.data.relational.core.dialect.HsqlDbDialect; +import org.springframework.data.relational.core.dialect.MariaDbDialect; import org.springframework.data.relational.core.dialect.MySqlDialect; import org.springframework.data.relational.core.dialect.OracleDialect; import org.springframework.data.relational.core.dialect.PostgresDialect; @@ -118,19 +123,22 @@ private static Dialect getDialect(Connection connection) throws SQLException { return HsqlDbDialect.INSTANCE; } if (name.contains("h2")) { - return H2Dialect.INSTANCE; + return JdbcH2Dialect.INSTANCE; } - if (name.contains("mysql") || name.contains("mariadb")) { - return new MySqlDialect(getIdentifierProcessing(metaData)); + if (name.contains("mysql")) { + return new JdbcMySqlDialect(getIdentifierProcessing(metaData)); + } + if (name.contains("mariadb")) { + return new MariaDbDialect(getIdentifierProcessing(metaData)); } if (name.contains("postgresql")) { return PostgresDialect.INSTANCE; } if (name.contains("microsoft")) { - return SqlServerDialect.INSTANCE; + return JdbcSqlServerDialect.INSTANCE; } if (name.contains("db2")) { - return Db2Dialect.INSTANCE; + return JdbcDb2Dialect.INSTANCE; } if (name.contains("oracle")) { return OracleDialect.INSTANCE; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java index ae85b337f0..e5d0c291f4 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java @@ -22,6 +22,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.time.OffsetDateTime; import java.util.HashMap; import java.util.Map; @@ -62,6 +63,7 @@ public final class JdbcUtil { sqlTypeMappings.put(Date.class, Types.DATE); sqlTypeMappings.put(Time.class, Types.TIME); sqlTypeMappings.put(Timestamp.class, Types.TIMESTAMP); + sqlTypeMappings.put(OffsetDateTime.class, Types.TIMESTAMP_WITH_TIMEZONE); } private JdbcUtil() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/JdbcH2DialectTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/JdbcH2DialectTests.java new file mode 100644 index 0000000000..511ecd3e28 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/JdbcH2DialectTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 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.dialect; + +import static org.assertj.core.api.Assertions.*; + +import java.time.OffsetDateTime; + +import org.h2.api.TimestampWithTimeZone; +import org.h2.util.DateTimeUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link JdbcH2Dialect}. + * + * @author Jens Schauder + */ +class JdbcH2DialectTests { + + @Test + void TimestampWithTimeZone2OffsetDateTimeConverterConvertsProperly() { + + JdbcH2Dialect.TimestampWithTimeZoneToOffsetDateTimeConverter converter = JdbcH2Dialect.TimestampWithTimeZoneToOffsetDateTimeConverter.INSTANCE; + long dateValue = 123456789; + long timeNanos = 987654321; + int timeZoneOffsetSeconds = 4 * 60 * 60; + TimestampWithTimeZone timestampWithTimeZone = new TimestampWithTimeZone(dateValue, timeNanos, + timeZoneOffsetSeconds); + + OffsetDateTime offsetDateTime = converter.convert(timestampWithTimeZone); + + assertThat(offsetDateTime.getOffset().getTotalSeconds()).isEqualTo(timeZoneOffsetSeconds); + assertThat(offsetDateTime.getNano()).isEqualTo(timeNanos); + assertThat(offsetDateTime.toEpochSecond()) + .isEqualTo(DateTimeUtils.getEpochSeconds(dateValue, timeNanos, timeZoneOffsetSeconds)); + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/OffsetDateTimeToTimestampConverterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/OffsetDateTimeToTimestampConverterUnitTests.java new file mode 100644 index 0000000000..cecf7dcbcd --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/OffsetDateTimeToTimestampConverterUnitTests.java @@ -0,0 +1,43 @@ +package org.springframework.data.jdbc.core.dialect; + +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.assertj.core.api.Assertions.*; + +/* + * Copyright 2021 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. + */ + +/** + * Tests for {@link JdbcDb2Dialect.OffsetDateTimeToTimestampConverter}. + * + * @author Jens Schauder + */ +class OffsetDateTimeToTimestampConverterUnitTests { + + @Test + void conversionPreservesInstant() { + + OffsetDateTime offsetDateTime = OffsetDateTime.of(5, 5, 5, 5,5,5,123456789, ZoneOffset.ofHours(3)); + + Timestamp timestamp = JdbcDb2Dialect.OffsetDateTimeToTimestampConverter.INSTANCE.convert(offsetDateTime); + + assertThat(timestamp.toInstant()).isEqualTo(offsetDateTime.toInstant()); + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index d5d73f7682..54497a5d44 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -27,14 +27,16 @@ import java.io.IOException; import java.sql.ResultSet; import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationListener; @@ -92,6 +94,9 @@ private static DummyEntity createDummyEntity() { @BeforeEach public void before() { + + repository.deleteAll(); + eventListener.events.clear(); } @@ -282,17 +287,7 @@ public void findByIdReturnsEmptyWhenNoneFound() { @Test // DATAJDBC-464, DATAJDBC-318 public void executeQueryWithParameterRequiringConversion() { - Instant now = Instant.now(); - - DummyEntity first = repository.save(createDummyEntity()); - first.setPointInTime(now.minusSeconds(1000L)); - first.setName("first"); - - DummyEntity second = repository.save(createDummyEntity()); - second.setPointInTime(now.plusSeconds(1000L)); - second.setName("second"); - - repository.saveAll(asList(first, second)); + Instant now = createDummyBeforeAndAfterNow(); assertThat(repository.after(now)) // .extracting(DummyEntity::getName) // @@ -408,7 +403,7 @@ public void countByQueryDerivation() { @Test // #945 @EnabledOnFeature(TestDatabaseFeatures.Feature.IS_POSTGRES) public void usePrimitiveArrayAsArgument() { - assertThat(repository.unnestPrimitive(new int[]{1, 2, 3})).containsExactly(1,2,3); + assertThat(repository.unnestPrimitive(new int[] { 1, 2, 3 })).containsExactly(1, 2, 3); } @Test // GH-774 @@ -442,6 +437,17 @@ public void sliceByNameShouldReturnCorrectResult() { assertThat(slice.hasNext()).isTrue(); } + @Test // #935 + public void queryByOffsetDateTime() { + + Instant now = createDummyBeforeAndAfterNow(); + OffsetDateTime timeArgument = OffsetDateTime.ofInstant(now, ZoneOffset.ofHours(2)); + + List entities = repository.findByOffsetDateTime(timeArgument); + + assertThat(entities).extracting(DummyEntity::getName).containsExactly("second"); + } + @Test // #971 public void stringQueryProjectionShouldReturnProjectedEntities() { @@ -486,6 +492,29 @@ public void pageQueryProjectionShouldReturnProjectedEntities() { assertThat(result.getContent().get(0).getName()).isEqualTo("Entity Name"); } + private Instant createDummyBeforeAndAfterNow() { + + Instant now = Instant.now(); + + DummyEntity first = createDummyEntity(); + Instant earlier = now.minusSeconds(1000L); + OffsetDateTime earlierPlus3 = earlier.atOffset(ZoneOffset.ofHours(3)); + first.setPointInTime(earlier); + first.offsetDateTime = earlierPlus3; + + first.setName("first"); + + DummyEntity second = createDummyEntity(); + Instant later = now.plusSeconds(1000L); + OffsetDateTime laterPlus3 = later.atOffset(ZoneOffset.ofHours(3)); + second.setPointInTime(later); + second.offsetDateTime = laterPlus3; + second.setName("second"); + + repository.saveAll(asList(first, second)); + return now; + } + interface DummyEntityRepository extends CrudRepository { List findAllByNamedQuery(); @@ -525,6 +554,9 @@ interface DummyEntityRepository extends CrudRepository { Page findPageProjectionByName(String name, Pageable pageable); Slice findSliceByNameContains(String name, Pageable pageable); + + @Query("SELECT * FROM DUMMY_ENTITY WHERE OFFSET_DATE_TIME > :threshhold") + List findByOffsetDateTime(@Param("threshhold") OffsetDateTime threshhold); } @Configuration @@ -573,6 +605,7 @@ public void onApplicationEvent(AbstractRelationalEvent event) { static class DummyEntity { String name; Instant pointInTime; + OffsetDateTime offsetDateTime; @Id private Long idProp; public DummyEntity(String name) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/support/JdbcUtilTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/support/JdbcUtilTests.java new file mode 100644 index 0000000000..371c752289 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/support/JdbcUtilTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 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.support; + +import static org.assertj.core.api.Assertions.*; + +import java.sql.Types; +import java.time.OffsetDateTime; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link JdbcUtil}. + * + * @author Jens Schauder + */ +class JdbcUtilTests { + + @Test + void test() { + assertThat(JdbcUtil.sqlTypeFor(OffsetDateTime.class)).isEqualTo(Types.TIMESTAMP_WITH_TIMEZONE); + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 20c73b859f..e74d30b0b2 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -15,6 +15,9 @@ */ package org.springframework.data.jdbc.testing; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; import javax.sql.DataSource; @@ -38,8 +41,10 @@ import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; import org.springframework.data.jdbc.repository.config.DialectResolver; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -57,6 +62,7 @@ * @author Mark Paluch * @author Fei Dong * @author Myeonghyeon Lee + * @author Christoph Strobl */ @Configuration @ComponentScan // To pick up configuration classes (per activated profile) @@ -108,8 +114,21 @@ JdbcMappingContext jdbcMappingContext(Optional namingStrategy, C } @Bean - CustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(); + CustomConversions jdbcCustomConversions(Dialect dialect) { + + SimpleTypeHolder simpleTypeHolder = dialect.simpleTypes().isEmpty() ? JdbcSimpleTypes.HOLDER + : new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER); + + return new JdbcCustomConversions(CustomConversions.StoreConversions.of(simpleTypeHolder, storeConverters(dialect)), + Collections.emptyList()); + } + + private List storeConverters(Dialect dialect) { + + List converters = new ArrayList<>(); + converters.addAll(dialect.getConverters()); + converters.addAll(JdbcCustomConversions.storeConverters()); + return converters; } @Bean diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index 358bcbbbd9..daa415344a 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -4,5 +4,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP -- with time zone is only supported with z/OS ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index 6649c1439d..b9b3101690 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -2,5 +2,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index 6649c1439d..b9b3101690 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -2,5 +2,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 83aea089d1..f9b086443b 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -2,5 +2,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP(3) + POINT_IN_TIME TIMESTAMP(3), + OFFSET_DATE_TIME TIMESTAMP(3) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index e632e642bd..c71942b4c1 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -3,5 +3,6 @@ CREATE TABLE dummy_entity ( id_Prop BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME DATETIME + POINT_IN_TIME DATETIME, + OFFSET_DATE_TIME DATETIMEOFFSET ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index 83aea089d1..9c4085b27a 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -1,6 +1,9 @@ +SET SQL_MODE='ALLOW_INVALID_DATES'; + CREATE TABLE dummy_entity ( id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP(3) + POINT_IN_TIME TIMESTAMP(3) default null, + OFFSET_DATE_TIME TIMESTAMP(3) default null ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index 28b2d80ec7..a3d831346d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -4,5 +4,6 @@ CREATE TABLE DUMMY_ENTITY ( ID_PROP NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, NAME VARCHAR2(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 803ef24751..5e670bfe77 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -3,5 +3,6 @@ CREATE TABLE dummy_entity ( id_Prop SERIAL PRIMARY KEY, NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP + POINT_IN_TIME TIMESTAMP, + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mariadb.sql index 8e100e80ae..c14f120013 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mariadb.sql @@ -1,2 +1,2 @@ -CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp DATETIME PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer DECIMAL(20), date DATETIME, local_Date_Time DATETIME, zoned_Date_Time VARCHAR(30)); -CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( id_Timestamp DATETIME NOT NULL PRIMARY KEY, data VARCHAR(100)); +CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp TIMESTAMP PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer DECIMAL(20), date DATETIME, local_Date_Time DATETIME, zoned_Date_Time VARCHAR(30)); +CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( id_Timestamp TIMESTAMP NOT NULL PRIMARY KEY, data VARCHAR(100)); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mysql.sql index 8e100e80ae..c14f120013 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-mysql.sql @@ -1,2 +1,2 @@ -CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp DATETIME PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer DECIMAL(20), date DATETIME, local_Date_Time DATETIME, zoned_Date_Time VARCHAR(30)); -CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( id_Timestamp DATETIME NOT NULL PRIMARY KEY, data VARCHAR(100)); +CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp TIMESTAMP PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer DECIMAL(20), date DATETIME, local_Date_Time DATETIME, zoned_Date_Time VARCHAR(30)); +CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( id_Timestamp TIMESTAMP NOT NULL PRIMARY KEY, data VARCHAR(100)); diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 4e42a006ec..4558427199 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.3.0-SNAPSHOT + 2.3.0-gh-935-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-gh-935-SNAPSHOT diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java index 8f4b57a9fd..82f527a148 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java @@ -15,6 +15,9 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collection; +import java.util.Collections; + import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.LockOptions; @@ -110,4 +113,9 @@ public Position getClausePosition() { public IdentifierProcessing getIdentifierProcessing() { return IdentifierProcessing.ANSI; } + + @Override + public Collection getConverters() { + return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java index 35097866d9..d12e54e19e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java @@ -15,6 +15,10 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.render.SelectRenderContext; @@ -27,6 +31,7 @@ * @author Mark Paluch * @author Jens Schauder * @author Myeonghyeon Lee + * @author Christoph Strobl * @since 1.1 */ public interface Dialect { @@ -82,7 +87,26 @@ default Escaper getLikeEscaper() { return Escaper.DEFAULT; } - default IdGeneration getIdGeneration(){ + default IdGeneration getIdGeneration() { return IdGeneration.DEFAULT; - }; + } + + /** + * Return a collection of converters for this dialect. + * + * @return a collection of converters for this dialect. + */ + default Collection getConverters() { + return Collections.emptySet(); + } + + /** + * Return the {@link Set} of types considered store native types that can be handeled by the driver. + * + * @return never {@literal null}. + * @since 2.3 + */ + default Set> simpleTypes() { + return Collections.emptySet(); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java index 74b8e90b8c..7444edde27 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java @@ -15,6 +15,9 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collections; +import java.util.Set; + import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; @@ -26,6 +29,7 @@ * * @author Mark Paluch * @author Myeonghyeon Lee + * @author Christph Strobl * @since 2.0 */ public class H2Dialect extends AbstractDialect { @@ -137,4 +141,21 @@ public Class getArrayType(Class userType) { public IdentifierProcessing getIdentifierProcessing() { return IdentifierProcessing.create(Quoting.ANSI, LetterCasing.UPPER_CASE); } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#simpleTypes() + */ + @Override + public Set> simpleTypes() { + + if (!ClassUtils.isPresent("org.h2.api.TimestampWithTimeZone", getClass().getClassLoader())) { + return Collections.emptySet(); + } + try { + return Collections.singleton(ClassUtils.forName("org.h2.api.TimestampWithTimeZone", getClass().getClassLoader())); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java new file mode 100644 index 0000000000..c983b37bc4 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2021 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.relational.core.dialect; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.data.relational.core.sql.IdentifierProcessing; + +/** + * A SQL dialect for MariaDb. + * + * @author Jens Schauder + * @since 2.3 + */ +public class MariaDbDialect extends MySqlDialect { + + public MariaDbDialect(IdentifierProcessing identifierProcessing) { + super(identifierProcessing); + } + + @Override + public Collection getConverters() { + return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java index 51b65d0018..6032582186 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java @@ -15,11 +15,14 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collection; +import java.util.Collections; + import org.springframework.data.relational.core.sql.IdentifierProcessing; +import org.springframework.data.relational.core.sql.LockOptions; import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; import org.springframework.util.Assert; -import org.springframework.data.relational.core.sql.LockOptions; /** * A SQL dialect for MySQL. @@ -161,4 +164,9 @@ public LockClause lock() { public IdentifierProcessing getIdentifierProcessing() { return identifierProcessing; } + + @Override + public Collection getConverters() { + return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java index ad9889f227..76f0c72cda 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java @@ -15,15 +15,8 @@ */ package org.springframework.data.relational.core.dialect; -import java.util.List; - -import org.springframework.data.relational.core.sql.IdentifierProcessing; -import org.springframework.data.relational.core.sql.LockOptions; -import org.springframework.data.relational.core.sql.Table; -import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; -import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; +import java.util.Collection; +import java.util.Collections; /** * An SQL dialect for Oracle. @@ -51,4 +44,10 @@ protected OracleDialect() {} public IdGeneration getIdGeneration() { return ID_GENERATION; } + + @Override + public Collection getConverters() { + return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE); + } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java index 08beb8b9dc..6c93a52d18 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java @@ -15,14 +15,16 @@ */ package org.springframework.data.relational.core.dialect; +import java.util.Collection; +import java.util.Collections; import java.util.List; import org.springframework.data.relational.core.sql.IdentifierProcessing; -import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; -import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; import org.springframework.data.relational.core.sql.LockOptions; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; +import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -113,6 +115,11 @@ public ArrayColumns getArraySupport() { return ARRAY_COLUMNS; } + @Override + public Collection getConverters() { + return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE); + } + static class PostgresLockClause implements LockClause { private final IdentifierProcessing identifierProcessing; @@ -165,7 +172,7 @@ public String getLock(LockOptions lockOptions) { public Position getClausePosition() { return Position.AFTER_ORDER_BY; } - }; + } static class PostgresArrayColumns implements ArrayColumns { @@ -195,4 +202,5 @@ public Class getArrayType(Class userType) { public IdentifierProcessing getIdentifierProcessing() { return IdentifierProcessing.create(Quoting.ANSI, LetterCasing.LOWER_CASE); } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/TimestampAtUtcToOffsetDateTimeConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/TimestampAtUtcToOffsetDateTimeConverter.java new file mode 100644 index 0000000000..7ec957c951 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/TimestampAtUtcToOffsetDateTimeConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 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.relational.core.dialect; + +import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.time.ZoneId; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; + +/** + * A reading convert to convert {@link Timestamp} to {@link OffsetDateTime}. For the conversion the {@link Timestamp} + * gets considered to be at UTC and the result of the conversion will have an offset of 0 and represent the same + * instant. + * + * @author Jens Schauder + * @since 2.3 + */ +@ReadingConverter +enum TimestampAtUtcToOffsetDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public OffsetDateTime convert(Timestamp timestamp) { + return OffsetDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC")); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/TimestampAtUtcToOffsetDateTimeConverterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/TimestampAtUtcToOffsetDateTimeConverterUnitTests.java new file mode 100644 index 0000000000..dee5e7befc --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/TimestampAtUtcToOffsetDateTimeConverterUnitTests.java @@ -0,0 +1,42 @@ +package org.springframework.data.relational.core.dialect; + +import static org.assertj.core.api.Assertions.*; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; + +import org.junit.jupiter.api.Test; + +/* + * Copyright 2021 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. + */ + +/** + * Tests {@link TimestampAtUtcToOffsetDateTimeConverter}. + * + * @author Jens Schauder + */ +class TimestampAtUtcToOffsetDateTimeConverterUnitTests { + + @Test + void conversionMaintainsInstant() { + + Timestamp timestamp = Timestamp.from(Instant.now()); + OffsetDateTime converted = TimestampAtUtcToOffsetDateTimeConverter.INSTANCE.convert(timestamp); + + assertThat(converted.toInstant()).isEqualTo(timestamp.toInstant()); + } +}