diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java index 960997c6a..18f9df232 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java @@ -17,6 +17,7 @@ package org.springframework.data.jdbc.core.convert; import java.sql.Array; import java.sql.JDBCType; +import java.util.function.Function; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.jdbc.core.ConnectionCallback; @@ -33,6 +34,7 @@ import org.springframework.util.Assert; public class DefaultJdbcTypeFactory implements JdbcTypeFactory { private final JdbcOperations operations; + private final Function jdbcTypeToSqlName; /** * Creates a new {@link DefaultJdbcTypeFactory}. @@ -40,10 +42,21 @@ public class DefaultJdbcTypeFactory implements JdbcTypeFactory { * @param operations must not be {@literal null}. */ public DefaultJdbcTypeFactory(JdbcOperations operations) { + this(operations, JDBCType::getName); + } + + /** + * Creates a new {@link DefaultJdbcTypeFactory}. + * + * @param operations must not be {@literal null}. + */ + public DefaultJdbcTypeFactory(JdbcOperations operations, Function jdbcTypeToSqlName) { Assert.notNull(operations, "JdbcOperations must not be null"); + Assert.notNull(jdbcTypeToSqlName, "JdbcTypeToSqlName must not be null"); this.operations = operations; + this.jdbcTypeToSqlName = jdbcTypeToSqlName; } @Override @@ -55,7 +68,7 @@ public class DefaultJdbcTypeFactory implements JdbcTypeFactory { JDBCType jdbcType = JdbcUtil.jdbcTypeFor(componentType); Assert.notNull(jdbcType, () -> String.format("Couldn't determine JDBCType for %s", componentType)); - String typeName = jdbcType.getName(); + String typeName = jdbcTypeToSqlName.apply(jdbcType); return operations.execute((ConnectionCallback) c -> c.createArrayOf(typeName, value)); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcArrayColumns.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcArrayColumns.java new file mode 100644 index 000000000..3c90c194b --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcArrayColumns.java @@ -0,0 +1,60 @@ +/* + * 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 org.springframework.data.relational.core.dialect.ArrayColumns; + +/** + * {@link org.springframework.data.relational.core.dialect.ArrayColumns} that offer JDBC specific functionality. + * + * @author Jens Schauder + * @since 2.3 + */ +public interface JdbcArrayColumns extends ArrayColumns { + + JdbcArrayColumns UNSUPPORTED = new JdbcArrayColumns() { + @Override + public boolean isSupported() { + return false; + } + + @Override + public Class getArrayType(Class userType) { + throw new UnsupportedOperationException("Array types not supported"); + } + + @Override + public String getSqlTypeRepresentation(JDBCType jdbcType) { + throw new UnsupportedOperationException("Array types not supported"); + } + }; + + /** + * The appropriate SQL type as a String which should be used to represent the given {@link JDBCType} in an + * {@link java.sql.Array}. Defaults to the name of the argument. + * + * @param jdbcType the {@link JDBCType} value representing the type that should be stored in the + * {@link java.sql.Array}. Must not be {@literal null}. + * @return the appropriate SQL type as a String which should be used to represent the given {@link JDBCType} in an + * {@link java.sql.Array}. Guaranteed to be not {@literal null}. + */ + default String getSqlTypeRepresentation(JDBCType jdbcType) { + return jdbcType.getName(); + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDialect.java new file mode 100644 index 000000000..0dc94d9aa --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDialect.java @@ -0,0 +1,37 @@ +/* + * 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 org.springframework.data.relational.core.dialect.Dialect; + +/** + * {@link org.springframework.data.relational.core.dialect.ArrayColumns} that offer JDBC specific functionality. + * + * @author Jens Schauder + * @since 2.3 + */ +public interface JdbcDialect extends Dialect { + + /** + * Returns the JDBC specific array support object that describes how array-typed columns are supported by this + * dialect. + * + * @return the JDBC specific array support object that describes how array-typed columns are supported by this + * dialect. + */ + @Override + JdbcArrayColumns getArraySupport(); +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java new file mode 100644 index 000000000..839f8ff79 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java @@ -0,0 +1,44 @@ +/* + * 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 org.springframework.data.relational.core.dialect.PostgresDialect; + +/** + * JDBC specific Postgres Dialect. + * + * @author Jens Schauder + * @since 2.3 + */ +public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect { + + public static final JdbcPostgresDialect INSTANCE = new JdbcPostgresDialect(); + private static final JdbcPostgresArrayColumns ARRAY_COLUMNS = new JdbcPostgresArrayColumns(); + + @Override + public JdbcArrayColumns getArraySupport() { + return ARRAY_COLUMNS; + } + + static class JdbcPostgresArrayColumns extends PostgresArrayColumns implements JdbcArrayColumns { + @Override + public String getSqlTypeRepresentation(JDBCType jdbcType) { + return jdbcType == JDBCType.DOUBLE ? "FLOAT8" : jdbcType.getName(); + } + } +} 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 7aca5b75f..9f239e2fb 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,10 +15,12 @@ */ package org.springframework.data.jdbc.repository.config; +import java.sql.JDBCType; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +33,6 @@ 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; @@ -42,12 +43,11 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; 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.dialect.JdbcDialect; 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; @@ -66,7 +66,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @Configuration(proxyBeanMethods = false) public class AbstractJdbcConfiguration implements ApplicationContextAware { - private static Logger LOG = LoggerFactory.getLogger(AbstractJdbcConfiguration.class); + private static final Logger LOG = LoggerFactory.getLogger(AbstractJdbcConfiguration.class); private ApplicationContext applicationContext; @@ -100,7 +100,11 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext, NamedParameterJdbcOperations operations, @Lazy RelationResolver relationResolver, JdbcCustomConversions conversions, Dialect dialect) { - DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations()); + Function jdbcTypeToSqlName = dialect instanceof JdbcDialect + ? ((JdbcDialect) dialect).getArraySupport()::getSqlTypeRepresentation + : JDBCType::getName; + DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations(), + jdbcTypeToSqlName); return new BasicJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory, dialect.getIdentifierProcessing()); @@ -120,7 +124,8 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { try { Dialect dialect = applicationContext.getBean(Dialect.class); - SimpleTypeHolder simpleTypeHolder = dialect.simpleTypes().isEmpty() ? JdbcSimpleTypes.HOLDER : new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER); + SimpleTypeHolder simpleTypeHolder = dialect.simpleTypes().isEmpty() ? JdbcSimpleTypes.HOLDER + : new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER); return new JdbcCustomConversions( CustomConversions.StoreConversions.of(simpleTypeHolder, storeConverters(dialect)), userConverters()); 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 d7f49fd65..2f7b36702 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 @@ -31,16 +31,12 @@ 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.JdbcPostgresDialect; 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; -import org.springframework.data.relational.core.dialect.SqlServerDialect; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.util.Optionals; import org.springframework.jdbc.core.ConnectionCallback; @@ -132,7 +128,7 @@ public class DialectResolver { return new MariaDbDialect(getIdentifierProcessing(metaData)); } if (name.contains("postgresql")) { - return PostgresDialect.INSTANCE; + return JdbcPostgresDialect.INSTANCE; } if (name.contains("microsoft")) { return JdbcSqlServerDialect.INSTANCE; @@ -144,7 +140,7 @@ public class DialectResolver { return OracleDialect.INSTANCE; } - LOG.info(String.format("Couldn't determine Dialect for \"%s\"", name) ); + LOG.info(String.format("Couldn't determine Dialect for \"%s\"", name)); return null; } 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 371c47b92..334dca890 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 @@ -555,6 +555,24 @@ public class JdbcAggregateTemplateIntegrationTests { assertThat(reloaded.digits).isEqualTo(Arrays.asList("one", "two", "three")); } + @Test // GH-1033 + @EnabledOnFeature(SUPPORTS_ARRAYS) + public void saveAndLoadAnEntityWithListOfDouble() { + + DoubleListOwner doubleListOwner = new DoubleListOwner(); + doubleListOwner.digits.addAll(Arrays.asList(1.2, 1.3, 1.4)); + + DoubleListOwner saved = template.save(doubleListOwner); + + assertThat(saved.id).isNotNull(); + + DoubleListOwner reloaded = template.findById(saved.id, DoubleListOwner.class); + + assertThat(reloaded).isNotNull(); + assertThat(reloaded.id).isEqualTo(saved.id); + assertThat(reloaded.digits).isEqualTo(Arrays.asList(1.2, 1.3, 1.4)); + } + @Test // DATAJDBC-259 @EnabledOnFeature(SUPPORTS_ARRAYS) public void saveAndLoadAnEntityWithSet() { @@ -911,7 +929,6 @@ public class JdbcAggregateTemplateIntegrationTests { List digits = new ArrayList<>(); } - @Table("ARRAY_OWNER") private static class SetOwner { @Id Long id; @@ -919,6 +936,13 @@ public class JdbcAggregateTemplateIntegrationTests { Set digits = new HashSet<>(); } + private static class DoubleListOwner { + + @Id Long id; + + List digits = new ArrayList<>(); + } + @Data static class LegoSet { 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 e74d30b0b..77f79f42f 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,10 +15,12 @@ */ package org.springframework.data.jdbc.testing; +import java.sql.JDBCType; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Function; import javax.sql.DataSource; @@ -40,6 +42,7 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; 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.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; import org.springframework.data.jdbc.repository.config.DialectResolver; @@ -136,11 +139,15 @@ public class TestConfiguration { CustomConversions conversions, @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, Dialect dialect) { + Function jdbcTypeToSqlName = dialect instanceof JdbcDialect + ? ((JdbcDialect) dialect).getArraySupport()::getSqlTypeRepresentation + : JDBCType::getName; + return new BasicJdbcConverter( // mappingContext, // relationResolver, // conversions, // - new DefaultJdbcTypeFactory(template.getJdbcOperations()), // + new DefaultJdbcTypeFactory(template.getJdbcOperations(), jdbcTypeToSqlName), // dialect.getIdentifierProcessing()); } 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 63294ab7d..8add25c8e 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 @@ -52,6 +52,12 @@ CREATE TABLE BYTE_ARRAY_OWNER BINARY_DATA BYTEA NOT NULL ); +CREATE TABLE DOUBLE_LIST_OWNER +( + ID SERIAL PRIMARY KEY, + DIGITS ARRAY[10] +); + CREATE TABLE CHAIN4 ( FOUR SERIAL PRIMARY KEY, 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 fda435ea4..96c5145e2 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 @@ -54,6 +54,12 @@ CREATE TABLE BYTE_ARRAY_OWNER BINARY_DATA VARBINARY(20) NOT NULL ); +CREATE TABLE DOUBLE_LIST_OWNER +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + DIGITS DOUBLE PRECISION ARRAY[10] +); + CREATE TABLE CHAIN4 ( FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql index 47c6841e6..e1990584a 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql @@ -61,6 +61,12 @@ CREATE TABLE "ARRAY_OWNER" MULTIDIMENSIONAL VARCHAR(20)[10][10] ); +CREATE TABLE DOUBLE_LIST_OWNER +( + ID SERIAL PRIMARY KEY, + DIGITS DOUBLE PRECISION[10] +); + CREATE TABLE BYTE_ARRAY_OWNER ( ID SERIAL PRIMARY KEY, 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 9495c2cd3..e43855826 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,6 +15,7 @@ */ package org.springframework.data.relational.core.dialect; +import java.sql.JDBCType; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -178,7 +179,7 @@ public class PostgresDialect extends AbstractDialect { } } - static class PostgresArrayColumns implements ArrayColumns { + protected static class PostgresArrayColumns implements ArrayColumns { /* * (non-Javadoc)