From ceb15fe826c07257b75aa033880230b23bbe0b27 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 26 Feb 2019 13:40:21 +0100 Subject: [PATCH] DATAJDBC-327 - Add support for conversion to JdbcValue and store byte[] as binary. Some JDBC drivers depend on correct explicit type information in order to pass on parameters to the database. So far CustomConversion had no way to provide that information. With this change one can use @WritingConverter that converts to JdbcTypeAware in order to provide that information. byte[] now also get stored as BINARY. Original pull request: #123. --- .../jdbc/core/DefaultDataAccessStrategy.java | 171 +++++++++++------- .../data/jdbc/core/convert/ArrayUtil.java | 42 +++++ .../jdbc/core/convert/BasicJdbcConverter.java | 115 ++++++++++-- .../core/convert/DefaultJdbcTypeFactory.java | 62 +++++++ .../data/jdbc/core/convert/JdbcConverter.java | 17 +- .../jdbc/core/convert/JdbcTypeFactory.java | 47 +++++ .../data/jdbc/core/convert/JdbcValue.java | 35 ++++ .../mybatis/MyBatisDataAccessStrategy.java | 8 +- .../config/AbstractJdbcConfiguration.java | 10 +- .../repository/config/JdbcConfiguration.java | 8 +- .../support/JdbcRepositoryFactoryBean.java | 6 +- .../data/jdbc/support/JdbcUtil.java | 53 ++++++ .../data/jdbc/support/package-info.java | 7 + .../DefaultDataAccessStrategyUnitTests.java | 16 +- ...JdbcAggregateTemplateIntegrationTests.java | 21 +++ ...lConverterAggregateReferenceUnitTests.java | 2 +- .../mybatis/MyBatisHsqlIntegrationTests.java | 3 +- ...itoryCustomConversionIntegrationTests.java | 157 ++++++++++++++++ .../SimpleJdbcRepositoryEventsUnitTests.java | 13 +- ...nableJdbcRepositoriesIntegrationTests.java | 3 +- .../JdbcRepositoryFactoryBeanUnitTests.java | 7 +- .../data/jdbc/testing/TestConfiguration.java | 23 ++- .../src/test/resources/logback.xml | 4 +- ...AggregateTemplateIntegrationTests-hsql.sql | 3 + ...regateTemplateIntegrationTests-mariadb.sql | 4 +- ...ggregateTemplateIntegrationTests-mssql.sql | 5 +- ...ggregateTemplateIntegrationTests-mysql.sql | 2 + ...egateTemplateIntegrationTests-postgres.sql | 8 + ...yCustomConversionIntegrationTests-hsql.sql | 1 + ...stomConversionIntegrationTests-mariadb.sql | 1 + ...CustomConversionIntegrationTests-mssql.sql | 1 + ...CustomConversionIntegrationTests-mysql.sql | 1 + ...tomConversionIntegrationTests-postgres.sql | 1 + src/main/asciidoc/jdbc.adoc | 6 + 34 files changed, 733 insertions(+), 130 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ArrayUtil.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcTypeFactory.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcValue.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/package-info.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mariadb.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mssql.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mysql.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index 4b3a48cfc..58b20c147 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -16,33 +16,35 @@ package org.springframework.data.jdbc.core; import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import java.sql.Connection; import java.sql.JDBCType; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; +import java.util.function.Predicate; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.JdbcValue; import org.springframework.data.jdbc.support.JdbcUtil; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyHandler; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.Identifier; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.KeyHolder; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -55,27 +57,32 @@ import org.springframework.util.Assert; * @author Thomas Lang * @author Bastian Wilhelm */ -@RequiredArgsConstructor public class DefaultDataAccessStrategy implements DataAccessStrategy { private final @NonNull SqlGeneratorSource sqlGeneratorSource; private final @NonNull RelationalMappingContext context; - private final @NonNull RelationalConverter converter; + private final @NonNull JdbcConverter converter; private final @NonNull NamedParameterJdbcOperations operations; private final @NonNull DataAccessStrategy accessStrategy; + public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, + JdbcConverter converter, NamedParameterJdbcOperations operations, @Nullable DataAccessStrategy accessStrategy) { + + this.sqlGeneratorSource = sqlGeneratorSource; + this.context = context; + this.converter = converter; + this.operations = operations; + this.accessStrategy = accessStrategy == null ? this : accessStrategy; + } + /** * Creates a {@link DefaultDataAccessStrategy} which references it self for resolution of recursive data accesses. * Only suitable if this is the only access strategy in use. */ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, - RelationalConverter converter, NamedParameterJdbcOperations operations) { + JdbcConverter converter, NamedParameterJdbcOperations operations) { - this.sqlGeneratorSource = sqlGeneratorSource; - this.operations = operations; - this.context = context; - this.converter = converter; - this.accessStrategy = this; + this(sqlGeneratorSource, context, converter, operations, null); } /* @@ -97,28 +104,19 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { KeyHolder holder = new GeneratedKeyHolder(); RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - Map parameters = new LinkedHashMap<>(identifier.size()); - identifier.forEach((name, value, type) -> { - parameters.put(name, converter.writeValue(value, ClassTypeInformation.from(type))); - }); + MapSqlParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", PersistentProperty::isIdProperty); - MapSqlParameterSource parameterSource = getPropertyMap(instance, persistentEntity, ""); + identifier.forEach((name, value, type) -> addConvertedPropertyValue(parameterSource, name, value, type)); Object idValue = getIdValueOrNull(instance, persistentEntity); - RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); - if (idValue != null) { - Assert.notNull(idProperty, "Since we have a non-null idValue, we must have an idProperty as well."); - - parameters.put(idProperty.getColumnName(), - converter.writeValue(idValue, ClassTypeInformation.from(idProperty.getColumnType()))); + RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); + addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); } - parameters.forEach(parameterSource::addValue); - operations.update( // - sql(domainType).getInsert(parameters.keySet()), // + sql(domainType).getInsert(new HashSet<>(Arrays.asList(parameterSource.getParameterNames()))), // parameterSource, // holder // ); @@ -135,7 +133,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - return operations.update(sql(domainType).getUpdate(), getPropertyMap(instance, persistentEntity, "")) != 0; + return operations.update(sql(domainType).getUpdate(), + getParameterSource(instance, persistentEntity, "", property -> false)) != 0; } /* @@ -240,17 +239,14 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @Override public Iterable findAllById(Iterable ids, Class domainType) { + RelationalPersistentProperty idProperty = getRequiredPersistentEntity(domainType).getRequiredIdProperty(); + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + addConvertedPropertyValuesAsList(parameterSource, idProperty, ids, "ids"); + String findAllInListSql = sql(domainType).getFindAllInList(); - Class targetType = getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType(); - MapSqlParameterSource parameter = new MapSqlParameterSource( // - "ids", // - StreamSupport.stream(ids.spliterator(), false) // - .map(id -> converter.writeValue(id, ClassTypeInformation.from(targetType))) // - .collect(Collectors.toList()) // - ); - - return operations.query(findAllInListSql, parameter, (RowMapper) getEntityRowMapper(domainType)); + return operations.query(findAllInListSql, parameterSource, (RowMapper) getEntityRowMapper(domainType)); } /* @@ -292,8 +288,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { return result; } - private MapSqlParameterSource getPropertyMap(S instance, RelationalPersistentEntity persistentEntity, - String prefix) { + private MapSqlParameterSource getParameterSource(S instance, RelationalPersistentEntity persistentEntity, + String prefix, Predicate skipProperty) { MapSqlParameterSource parameters = new MapSqlParameterSource(); @@ -301,6 +297,9 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { persistentEntity.doWithProperties((PropertyHandler) property -> { + if (skipProperty.test(property)) { + return; + } if (property.isEntity() && !property.isEmbedded()) { return; } @@ -309,42 +308,21 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { Object value = propertyAccessor.getProperty(property); RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); - MapSqlParameterSource additionalParameters = getPropertyMap((T) value, - (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix()); + MapSqlParameterSource additionalParameters = getParameterSource((T) value, + (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty); parameters.addValues(additionalParameters.getValues()); } else { Object value = propertyAccessor.getProperty(property); - Object convertedValue = convertForWrite(property, value); + String paramName = prefix + property.getColumnName(); - parameters.addValue(prefix + property.getColumnName(), convertedValue, - JdbcUtil.sqlTypeFor(property.getColumnType())); + addConvertedPropertyValue(parameters, property, value, paramName); } }); return parameters; } - @Nullable - private Object convertForWrite(RelationalPersistentProperty property, @Nullable Object value) { - - Object convertedValue = converter.writeValue(value, ClassTypeInformation.from(property.getColumnType())); - - if (convertedValue == null || !convertedValue.getClass().isArray()) { - return convertedValue; - } - - Class componentType = convertedValue.getClass(); - while (componentType.isArray()) { - componentType = componentType.getComponentType(); - } - - String typeName = JDBCType.valueOf(JdbcUtil.sqlTypeFor(componentType)).getName(); - - return operations.getJdbcOperations() - .execute((Connection c) -> c.createArrayOf(typeName, (Object[]) convertedValue)); - } - @SuppressWarnings("unchecked") @Nullable private ID getIdValueOrNull(S instance, RelationalPersistentEntity persistentEntity) { @@ -398,8 +376,65 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { - Class columnType = getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType(); - return new MapSqlParameterSource("id", converter.writeValue(id, ClassTypeInformation.from(columnType))); + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + addConvertedPropertyValue( // + parameterSource, // + getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // + id, // + "id" // + ); + return parameterSource; + } + + private void addConvertedPropertyValue(MapSqlParameterSource parameterSource, RelationalPersistentProperty property, + Object value, String paramName) { + + JdbcValue jdbcValue = converter.writeJdbcValue( // + value, // + property.getColumnType(), // + property.getSqlType() // + ); + + parameterSource.addValue(paramName, jdbcValue.getValue(), JdbcUtil.sqlTypeFor(jdbcValue.getJdbcType())); + } + + private void addConvertedPropertyValue(MapSqlParameterSource parameterSource, String name, Object value, + Class type) { + + JdbcValue jdbcValue = converter.writeJdbcValue( // + value, // + type, // + JdbcUtil.sqlTypeFor(type) // + ); + + parameterSource.addValue( // + name, // + jdbcValue.getValue(), // + JdbcUtil.sqlTypeFor(jdbcValue.getJdbcType()) // + ); + } + + private void addConvertedPropertyValuesAsList(MapSqlParameterSource parameterSource, + RelationalPersistentProperty property, Iterable values, String paramName) { + + List convertedIds = new ArrayList<>(); + JdbcValue jdbcValue = null; + for (Object id : values) { + + Class columnType = property.getColumnType(); + int sqlType = property.getSqlType(); + + jdbcValue = converter.writeJdbcValue(id, columnType, sqlType); + convertedIds.add(jdbcValue.getValue()); + } + + Assert.notNull(jdbcValue, "JdbcValue must be not null at this point. Please report this as a bug."); + + JDBCType jdbcType = jdbcValue.getJdbcType(); + int typeNumber = jdbcType == null ? JdbcUtils.TYPE_UNKNOWN : jdbcType.getVendorTypeNumber(); + + parameterSource.addValue(paramName, convertedIds, typeNumber); } @SuppressWarnings("unchecked") diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ArrayUtil.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ArrayUtil.java new file mode 100644 index 000000000..69f211915 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ArrayUtil.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.convert; + +import lombok.experimental.UtilityClass; + +/** + * A collection of utility methods for dealing with arrays. + * + * @author Jens Schauder + */ +@UtilityClass +class ArrayUtil { + + /** + * Convertes an {@code Byte[]} into a {@code byte[]} + * @param byteArray the array to be converted. Must not be {@literal null}. + * + * @return a {@code byte[]} of same size with the unboxed values of the input array. Guaranteed to be not {@literal null}. + */ + static Object toPrimitiveByteArray(Byte[] byteArray) { + + byte[] bytes = new byte[byteArray.length]; + for (int i = 0; i < byteArray.length; i++) { + bytes[i] = byteArray[i]; + } + return bytes; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java index e94f91bf9..5cb74c074 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java @@ -15,29 +15,27 @@ */ package org.springframework.data.jdbc.core.convert; +import java.sql.Array; +import java.sql.JDBCType; +import java.sql.SQLException; +import java.util.Optional; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.mapping.AggregateReference; -import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; -import org.springframework.data.relational.domain.Identifier; +import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; -import java.sql.Array; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - /** * {@link RelationalConverter} that uses a {@link MappingContext} to apply basic conversion of relational values to * property values. @@ -54,14 +52,45 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc private static final Logger LOG = LoggerFactory.getLogger(BasicJdbcConverter.class); + private final JdbcTypeFactory typeFactory; + /** * Creates a new {@link BasicRelationalConverter} given {@link MappingContext}. * * @param context must not be {@literal null}. org.springframework.data.jdbc.core.DefaultDataAccessStrategyUnitTests + * @param typeFactory */ public BasicJdbcConverter( - MappingContext, ? extends RelationalPersistentProperty> context) { + MappingContext, ? extends RelationalPersistentProperty> context, + JdbcTypeFactory typeFactory) { super(context); + this.typeFactory = typeFactory; + } + + /** + * Creates a new {@link BasicRelationalConverter} given {@link MappingContext} and {@link CustomConversions}. + * + * @param context must not be {@literal null}. + * @param conversions must not be {@literal null}. + * @param typeFactory + */ + public BasicJdbcConverter( + MappingContext, ? extends RelationalPersistentProperty> context, + CustomConversions conversions, JdbcTypeFactory typeFactory) { + super(context, conversions); + this.typeFactory = typeFactory; + } + + /** + * Creates a new {@link BasicRelationalConverter} given {@link MappingContext}. + * + * @param context must not be {@literal null}. org.springframework.data.jdbc.core.DefaultDataAccessStrategyUnitTests + * @deprecated use one of the constructors with {@link JdbcTypeFactory} parameter. + */ + @Deprecated + public BasicJdbcConverter( + MappingContext, ? extends RelationalPersistentProperty> context) { + this(context, JdbcTypeFactory.unsupported()); } /** @@ -69,11 +98,13 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc * * @param context must not be {@literal null}. * @param conversions must not be {@literal null}. + * @deprecated use one of the constructors with {@link JdbcTypeFactory} parameter. */ + @Deprecated public BasicJdbcConverter( MappingContext, ? extends RelationalPersistentProperty> context, CustomConversions conversions) { - super(context, conversions); + this(context, conversions, JdbcTypeFactory.unsupported()); } /* @@ -102,7 +133,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc if (value instanceof Array) { try { return readValue(((Array) value).getArray(), type); - } catch (SQLException | ConverterNotFoundException e ) { + } catch (SQLException | ConverterNotFoundException e) { LOG.info("Failed to extract a value of type %s from an Array. Attempting to use standard conversions.", e); } } @@ -129,5 +160,65 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return super.writeValue(value, type); } + private boolean canWriteAsJdbcValue(@Nullable Object value) { + if (value == null) { + return true; + } + + if (AggregateReference.class.isAssignableFrom(value.getClass())) { + return canWriteAsJdbcValue(((AggregateReference) value).getId()); + } + + RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(value.getClass()); + + if (persistentEntity != null) { + + Object id = persistentEntity.getIdentifierAccessor(value).getIdentifier(); + return canWriteAsJdbcValue(id); + } + + if (value instanceof JdbcValue) { + return true; + } + + Optional> customWriteTarget = getConversions().getCustomWriteTarget(value.getClass()); + return customWriteTarget.isPresent() && customWriteTarget.get().isAssignableFrom(JdbcValue.class); + } + + public JdbcValue writeJdbcValue(@Nullable Object value, Class columnType, int sqlType) { + + JdbcValue jdbcValue = tryToConvertToJdbcValue(value); + if (jdbcValue != null) { + return jdbcValue; + } + + Object convertedValue = writeValue(value, ClassTypeInformation.from(columnType)); + + if (convertedValue == null || !convertedValue.getClass().isArray()) { + return JdbcValue.of(convertedValue, JdbcUtil.jdbcTypeFor(sqlType)); + } + + Class componentType = convertedValue.getClass().getComponentType(); + if (componentType != byte.class && componentType != Byte.class) { + return JdbcValue.of(typeFactory.createArray((Object[]) convertedValue), JDBCType.ARRAY); + } + + if (componentType == Byte.class) { + convertedValue = ArrayUtil.toPrimitiveByteArray((Byte[]) convertedValue); + } + + return JdbcValue.of(convertedValue, JDBCType.BINARY); + + } + + private JdbcValue tryToConvertToJdbcValue(@Nullable Object value) { + + JdbcValue jdbcValue = null; + if (canWriteAsJdbcValue(value)) { + jdbcValue = (JdbcValue) writeValue(value, ClassTypeInformation.from(JdbcValue.class)); + } + + return jdbcValue; + } } 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 new file mode 100644 index 000000000..01eed1474 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.convert; + +import org.springframework.data.jdbc.support.JdbcUtil; +import org.springframework.jdbc.core.ConnectionCallback; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.util.Assert; + +import java.sql.Array; +import java.sql.JDBCType; + +/** + * A {@link JdbcTypeFactory} that performs the conversion by utilizing {@link JdbcOperations#execute(ConnectionCallback)}. + * + * @author Jens Schauder + * @since 1.1 + */ +public class DefaultJdbcTypeFactory implements JdbcTypeFactory { + + private final JdbcOperations operations; + + public DefaultJdbcTypeFactory(JdbcOperations operations) { + this.operations = operations; + } + + @Override + public Array createArray(Object[] value) { + + Assert.notNull(value, "Value must not be null."); + + Class componentType = innermostComponentType(value); + + JDBCType jdbcType = JdbcUtil.jdbcTypeFor(componentType); + Assert.notNull(jdbcType, () -> String.format("Couldn't determine JDBCType for %s", componentType)); + String typeName = jdbcType.getName(); + + return operations.execute((ConnectionCallback) c -> c.createArrayOf(typeName, value)); + } + + private static Class innermostComponentType(Object convertedValue) { + + Class componentType = convertedValue.getClass(); + while (componentType.isArray()) { + componentType = componentType.getComponentType(); + } + return componentType; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java index 2d460ed54..8908bf351 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java @@ -16,13 +16,26 @@ package org.springframework.data.jdbc.core.convert; import org.springframework.data.relational.core.conversion.RelationalConverter; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; /** * A {@link JdbcConverter} is responsible for converting for values to the native relational representation and vice * versa. * * @author Jens Schauder - * * @since 1.1 */ -public interface JdbcConverter extends RelationalConverter {} +public interface JdbcConverter extends RelationalConverter { + + /** + * Convert a property value into a {@link JdbcValue} that contains the converted value and information how to bind + * it to JDBC parameters. + * + * @param value a value as it is used in the object model. May be {@code null}. + * @param type {@link TypeInformation} into which the value is to be converted. Must not be {@code null}. + * @param sqlType the type constant from {@link java.sql.Types} to be used if non is specified by a converter. + * @return The converted value wrapped in a {@link JdbcValue}. Guaranteed to be not {@literal null}. + */ + JdbcValue writeJdbcValue(@Nullable Object value, Class type, int sqlType); +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcTypeFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcTypeFactory.java new file mode 100644 index 000000000..cbb78dce3 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcTypeFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.convert; + +import java.sql.Array; + +/** + * Allows the creation of instances of database dependent types, e.g. {@link Array}. + * + * @author Jens Schauder + * @since 1.1 + */ +public interface JdbcTypeFactory { + + /** + * An implementation used in places where a proper {@code JdbcTypeFactory} can not be provided but an instance needs + * to be provided anyway, mostly for providing backward compatibility. Calling it will result in an exception. The + * features normally supported by a {@link JdbcTypeFactory} will not work. + */ + static JdbcTypeFactory unsupported() { + + return value -> { + throw new UnsupportedOperationException("This JdbcTypeFactory does not support Array creation"); + }; + } + + /** + * Converts the provided value in a {@link Array} instance. + * + * @param value the value to be converted. Must not be {@literal null}. + * @return an {@link Array}. Guaranteed to be not {@literal null}. + */ + Array createArray(Object[] value); +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcValue.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcValue.java new file mode 100644 index 000000000..a2716448f --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcValue.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.convert; + +import lombok.Value; + +import java.sql.JDBCType; + +/** + * Wraps a value with the JDBCType that should be used to pass it as a bind parameter to a + * {@link java.sql.PreparedStatement}. Register a converter from any type to {@link JdbcValue} in order to control + * the value and the {@link JDBCType} as which a value should get passed to the JDBC driver. + * + * @author Jens Schauder + * @since 1.1 + */ +@Value(staticConstructor = "of") +public class JdbcValue { + + Object value; + JDBCType jdbcType; +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 0677a15e3..c0dbb7125 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -28,9 +28,9 @@ import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.Identifier; @@ -61,7 +61,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { * uses a {@link DefaultDataAccessStrategy} */ public static DataAccessStrategy createCombinedAccessStrategy(RelationalMappingContext context, - RelationalConverter converter, NamedParameterJdbcOperations operations, SqlSession sqlSession) { + JdbcConverter converter, NamedParameterJdbcOperations operations, SqlSession sqlSession) { return createCombinedAccessStrategy(context, converter, operations, sqlSession, NamespaceStrategy.DEFAULT_INSTANCE); } @@ -70,7 +70,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { * uses a {@link DefaultDataAccessStrategy} */ public static DataAccessStrategy createCombinedAccessStrategy(RelationalMappingContext context, - RelationalConverter converter, NamedParameterJdbcOperations operations, SqlSession sqlSession, + JdbcConverter converter, NamedParameterJdbcOperations operations, SqlSession sqlSession, NamespaceStrategy namespaceStrategy) { // the DefaultDataAccessStrategy needs a reference to the returned DataAccessStrategy. This creates a dependency @@ -104,7 +104,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { * transaction. Note that the resulting {@link DataAccessStrategy} only handles MyBatis. It does not include the * functionality of the {@link org.springframework.data.jdbc.core.DefaultDataAccessStrategy} which one normally still * wants. Use - * {@link #createCombinedAccessStrategy(RelationalMappingContext, RelationalConverter, NamedParameterJdbcOperations, SqlSession, NamespaceStrategy)} + * {@link #createCombinedAccessStrategy(RelationalMappingContext, JdbcConverter, NamedParameterJdbcOperations, SqlSession, NamespaceStrategy)} * to create such a {@link DataAccessStrategy}. * * @param sqlSession Must be non {@literal null}. 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 cf471c363..ef1eead66 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 @@ -26,12 +26,14 @@ import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** @@ -71,13 +73,13 @@ public abstract class AbstractJdbcConfiguration { * @return must not be {@literal null}. */ @Bean - public JdbcConverter jdbcConverter(RelationalMappingContext mappingContext) { - return new BasicJdbcConverter(mappingContext, jdbcCustomConversions()); + public JdbcConverter jdbcConverter(RelationalMappingContext mappingContext, JdbcOperations operations) { + return new BasicJdbcConverter(mappingContext, jdbcCustomConversions(), new DefaultJdbcTypeFactory(operations)); } /** * Register custom {@link Converter}s in a {@link JdbcCustomConversions} object if required. These - * {@link JdbcCustomConversions} will be registered with the {@link #relationalConverter(RelationalMappingContext)}. + * {@link JdbcCustomConversions} will be registered with the {@link #jdbcConverter(RelationalMappingContext, JdbcOperations)}. * Returns an empty {@link JdbcCustomConversions} instance by default. * * @return must not be {@literal null}. @@ -100,7 +102,7 @@ public abstract class AbstractJdbcConfiguration { */ @Bean public JdbcAggregateTemplate jdbcAggregateTemplate(ApplicationEventPublisher publisher, - RelationalMappingContext context, RelationalConverter converter, NamedParameterJdbcOperations operations) { + RelationalMappingContext context, JdbcConverter converter, NamedParameterJdbcOperations operations) { DataAccessStrategy dataAccessStrategy = new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, operations); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java index 31ade20c7..220df1300 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java @@ -27,7 +27,9 @@ import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.convert.JdbcTypeFactory; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; @@ -42,7 +44,6 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; * @author Mark Paluch * @author Michael Simons * @author Christoph Strobl - * * @deprecated Use {@link AbstractJdbcConfiguration} instead. */ @Configuration @@ -74,7 +75,8 @@ public class JdbcConfiguration { */ @Bean public RelationalConverter relationalConverter(RelationalMappingContext mappingContext) { - return new BasicJdbcConverter(mappingContext, jdbcCustomConversions()); + + return new BasicJdbcConverter(mappingContext, jdbcCustomConversions(), JdbcTypeFactory.unsupported()); } /** @@ -101,7 +103,7 @@ public class JdbcConfiguration { */ @Bean public JdbcAggregateOperations jdbcAggregateOperations(ApplicationEventPublisher publisher, - RelationalMappingContext context, RelationalConverter converter, NamedParameterJdbcOperations operations) { + RelationalMappingContext context, JdbcConverter converter, NamedParameterJdbcOperations operations) { DataAccessStrategy dataAccessStrategy = new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, operations); return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index 4318c582a..5cdf430f2 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -24,9 +24,9 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.RowMapperMap; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -50,7 +50,7 @@ public class JdbcRepositoryFactoryBean, S, ID extend private ApplicationEventPublisher publisher; private BeanFactory beanFactory; private RelationalMappingContext mappingContext; - private RelationalConverter converter; + private JdbcConverter converter; private DataAccessStrategy dataAccessStrategy; private QueryMappingConfiguration queryMappingConfiguration = QueryMappingConfiguration.EMPTY; private NamedParameterJdbcOperations operations; @@ -128,7 +128,7 @@ public class JdbcRepositoryFactoryBean, S, ID extend } @Autowired - public void setConverter(RelationalConverter converter) { + public void setConverter(JdbcConverter converter) { this.converter = converter; } 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 887f8e456..f1cc782ee 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 @@ -20,6 +20,7 @@ import lombok.experimental.UtilityClass; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Date; +import java.sql.JDBCType; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; @@ -27,6 +28,8 @@ import java.util.HashMap; import java.util.Map; import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Contains methods dealing with the quirks of JDBC, independent of any Entity, Aggregate or Repository abstraction. @@ -64,11 +67,61 @@ public class JdbcUtil { sqlTypeMappings.put(Timestamp.class, Types.TIMESTAMP); } + /** + * Returns the {@link Types} value suitable for passing a value of the provided type to a + * {@link java.sql.PreparedStatement}. + * + * @param type The type of value to be bound to a {@link java.sql.PreparedStatement}. + * @return One of the values defined in {@link Types} or {@link JdbcUtils#TYPE_UNKNOWN}. + */ public static int sqlTypeFor(Class type) { + + Assert.notNull(type, "Type must not be null."); + return sqlTypeMappings.keySet().stream() // .filter(k -> k.isAssignableFrom(type)) // .findFirst() // .map(sqlTypeMappings::get) // .orElse(JdbcUtils.TYPE_UNKNOWN); } + + /** + * Converts a {@link JDBCType} to an {@code int} value as defined in {@link Types}. + * + * @param jdbcType value to be converted. May be {@literal null}. + * @return One of the values defined in {@link Types} or {@link JdbcUtils#TYPE_UNKNOWN}. + */ + public static int sqlTypeFor(@Nullable JDBCType jdbcType) { + return jdbcType == null ? JdbcUtils.TYPE_UNKNOWN : jdbcType.getVendorTypeNumber(); + } + + /** + * Converts a value defined in {@link Types} into a {@link JDBCType} instance or {@literal null} if the value is + * {@link JdbcUtils#TYPE_UNKNOWN} + * + * @param sqlType One of the values defined in {@link Types} or {@link JdbcUtils#TYPE_UNKNOWN}. + * @return a matching {@link JDBCType} instance or {@literal null}. + */ + @Nullable + public static JDBCType jdbcTypeFor(int sqlType) { + + if (sqlType == JdbcUtils.TYPE_UNKNOWN) { + return null; + } + + return JDBCType.valueOf(sqlType); + } + + /** + * Returns the {@link JDBCType} suitable for passing a value of the provided type to a + * {@link java.sql.PreparedStatement}. + * + * @param type The type of value to be bound to a {@link java.sql.PreparedStatement}. + * @return a matching {@link JDBCType} instance or {@literal null}. + */ + @Nullable + public static JDBCType jdbcTypeFor(Class type) { + + return jdbcTypeFor(sqlTypeFor(type)); + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/package-info.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/package-info.java new file mode 100644 index 000000000..9790f9ded --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/package-info.java @@ -0,0 +1,7 @@ +/** + * @author Jens Schauder + */ +@NonNullApi +package org.springframework.data.jdbc.support; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java index 08b0ad76a..621c21944 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java @@ -16,8 +16,7 @@ package org.springframework.data.jdbc.core; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import lombok.AllArgsConstructor; @@ -32,10 +31,11 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; +import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.relational.core.conversion.BasicRelationalConverter; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.SqlParameterSource; @@ -54,7 +54,8 @@ public class DefaultDataAccessStrategyUnitTests { NamedParameterJdbcOperations jdbcOperations = mock(NamedParameterJdbcOperations.class); RelationalMappingContext context = new JdbcMappingContext(); - RelationalConverter converter = new BasicRelationalConverter(context, new JdbcCustomConversions()); + JdbcConverter converter = new BasicJdbcConverter(context, new JdbcCustomConversions(), + new DefaultJdbcTypeFactory(jdbcOperations.getJdbcOperations())); HashMap additionalParameters = new HashMap<>(); ArgumentCaptor paramSourceCaptor = ArgumentCaptor.forClass(SqlParameterSource.class); @@ -95,8 +96,9 @@ public class DefaultDataAccessStrategyUnitTests { @Test // DATAJDBC-235 public void considersConfiguredWriteConverter() { - RelationalConverter converter = new BasicRelationalConverter(context, - new JdbcCustomConversions(Arrays.asList(BooleanToStringConverter.INSTANCE, StringToBooleanConverter.INSTANCE))); + JdbcConverter converter = new BasicJdbcConverter(context, + new JdbcCustomConversions(Arrays.asList(BooleanToStringConverter.INSTANCE, StringToBooleanConverter.INSTANCE)), + new DefaultJdbcTypeFactory(jdbcOperations.getJdbcOperations())); DefaultDataAccessStrategy accessStrategy = new DefaultDataAccessStrategy( // new SqlGeneratorSource(context), // 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 2fc7f427c..379aaa72e 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 @@ -419,6 +419,21 @@ public class JdbcAggregateTemplateIntegrationTests { assertThat(reloaded.digits).isEqualTo(new HashSet<>(Arrays.asList("one", "two", "three"))); } + @Test // DATAJDBC-327 + public void saveAndLoadAnEntityWithByteArray() { + ByteArrayOwner owner = new ByteArrayOwner(); + owner.binaryData = new byte[]{1, 23, 42}; + + ByteArrayOwner saved = template.save(owner); + + ByteArrayOwner reloaded = template.findById(saved.id, ByteArrayOwner.class); + + assertThat(reloaded).isNotNull(); + assertThat(reloaded.id).isEqualTo(saved.id); + assertThat(reloaded.binaryData).isEqualTo(new byte[]{1, 23, 42}); + } + + private static void assumeNot(String dbProfileName) { Assume.assumeTrue("true" @@ -433,6 +448,12 @@ public class JdbcAggregateTemplateIntegrationTests { String[][] multidimensional; } + private static class ByteArrayOwner { + @Id Long id; + + byte[] binaryData; + } + @Table("ARRAY_OWNER") private static class ListOwner { @Id Long id; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java index c4296f978..1fb1cb708 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java @@ -43,7 +43,7 @@ public class BasicRelationalConverterAggregateReferenceUnitTests { ConversionService conversionService = new DefaultConversionService(); JdbcMappingContext context = new JdbcMappingContext(); - RelationalConverter converter = new BasicJdbcConverter(context); + RelationalConverter converter = new BasicJdbcConverter(context, JdbcTypeFactory.unsupported()); RelationalPersistentEntity entity = context.getRequiredPersistentEntity(DummyEntity.class); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java index db310f16f..1d15ba2fa 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java @@ -32,6 +32,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.data.jdbc.core.DataAccessStrategy; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.relational.core.conversion.RelationalConverter; @@ -90,7 +91,7 @@ public class MyBatisHsqlIntegrationTests { @Bean @Primary - DataAccessStrategy dataAccessStrategy(RelationalMappingContext context, RelationalConverter converter, + DataAccessStrategy dataAccessStrategy(RelationalMappingContext context, JdbcConverter converter, SqlSession sqlSession, EmbeddedDatabase db) { return MyBatisDataAccessStrategy.createCombinedAccessStrategy(context, converter, diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java new file mode 100644 index 000000000..b2bc4ac5b --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2017-2018 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 + * + * http://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.repository; + +import static java.util.Arrays.*; +import static org.assertj.core.api.Assertions.*; + +import java.math.BigDecimal; +import java.sql.JDBCType; +import java.util.Optional; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.convert.JdbcValue; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.CrudRepository; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.rules.SpringClassRule; +import org.springframework.test.context.junit4.rules.SpringMethodRule; +import org.springframework.transaction.annotation.Transactional; + +/** + * Tests storing and retrieving data types that get processed by custom conversions. + * + * @author Jens Schauder + */ +@ContextConfiguration +@Transactional +public class JdbcRepositoryCustomConversionIntegrationTests { + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return JdbcRepositoryCustomConversionIntegrationTests.class; + } + + @Bean + EntityWithBooleanRepository repository() { + return factory.getRepository(EntityWithBooleanRepository.class); + } + + @Bean + JdbcCustomConversions jdbcCustomConversions() { + return new JdbcCustomConversions(asList(BigDecimalToString.INSTANCE, StringToBigDecimalConverter.INSTANCE)); + } + } + + @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); + @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + + @Autowired EntityWithBooleanRepository repository; + + /** + * In PostrgreSQL this fails if a simple converter like the following is used. + * + *
+	 * {@code
+	 @WritingConverter enum PlainStringToBigDecimalConverter implements Converter {
+	
+	 	INSTANCE;
+	
+	 	@Override
+	 	@Nullable
+	 	public BigDecimal convert(@Nullable String source) {
+	
+	 		return source == null ? null : new BigDecimal(source);
+	 	}
+	
+	 }
+	}
+	 * 
+ */ + + @Test // DATAJDBC-327 + public void saveAndLoadAnEntity() { + + EntityWithStringyBigDecimal entity = new EntityWithStringyBigDecimal(); + entity.stringyNumber = "123456.78910"; + + repository.save(entity); + + Optional reloaded = repository.findById(entity.id); + + // loading the number from the database might result in additional zeros at the end. + String stringyNumber = reloaded.get().stringyNumber; + assertThat(stringyNumber).startsWith(entity.stringyNumber); + assertThat(stringyNumber.substring(entity.stringyNumber.length())).matches("0*"); + } + + interface EntityWithBooleanRepository extends CrudRepository {} + + private static class EntityWithStringyBigDecimal { + + @Id Long id; + String stringyNumber; + } + + @WritingConverter + enum StringToBigDecimalConverter implements Converter { + + INSTANCE; + + @Override + public JdbcValue convert(@Nullable String source) { + + Object value = source == null ? null : new BigDecimal(source); + return JdbcValue.of(value, JDBCType.DECIMAL); + } + + } + + @ReadingConverter + enum BigDecimalToString implements Converter { + + INSTANCE; + + @Override + public String convert(@Nullable BigDecimal source) { + + if (source == null) { + return null; + } + + return source.toString(); + } + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index 2610ac6a0..ed64d1c1c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -17,9 +17,7 @@ package org.springframework.data.jdbc.repository; import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import junit.framework.AssertionFailedError; @@ -39,12 +37,13 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; -import org.springframework.data.relational.core.conversion.BasicRelationalConverter; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.event.AfterDeleteEvent; import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; @@ -76,9 +75,9 @@ public class SimpleJdbcRepositoryEventsUnitTests { public void before() { RelationalMappingContext context = new JdbcMappingContext(); - RelationalConverter converter = new BasicRelationalConverter(context, new JdbcCustomConversions()); - NamedParameterJdbcOperations operations = createIdGeneratingOperations(); + JdbcConverter converter = new BasicJdbcConverter(context, new JdbcCustomConversions(), + new DefaultJdbcTypeFactory(operations.getJdbcOperations())); SqlGeneratorSource generatorSource = new SqlGeneratorSource(context); this.dataAccessStrategy = spy(new DefaultDataAccessStrategy(generatorSource, context, converter, operations)); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index 6e0dca18f..f312a1e40 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -34,6 +34,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; @@ -149,7 +150,7 @@ public class EnableJdbcRepositoriesIntegrationTests { @Bean("qualifierDataAccessStrategy") DataAccessStrategy defaultDataAccessStrategy(@Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, - RelationalMappingContext context, RelationalConverter converter) { + RelationalMappingContext context, JdbcConverter converter) { return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, template); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java index f3590d6f7..97797dd03 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java @@ -33,9 +33,10 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.JdbcTypeFactory; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; -import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.CrudRepository; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -82,7 +83,7 @@ public class JdbcRepositoryFactoryBeanUnitTests { factoryBean.setDataAccessStrategy(dataAccessStrategy); factoryBean.setMappingContext(mappingContext); - factoryBean.setConverter(new BasicRelationalConverter(mappingContext)); + factoryBean.setConverter(new BasicJdbcConverter(mappingContext, JdbcTypeFactory.unsupported())); factoryBean.setApplicationEventPublisher(publisher); factoryBean.setBeanFactory(beanFactory); factoryBean.afterPropertiesSet(); @@ -109,7 +110,7 @@ public class JdbcRepositoryFactoryBeanUnitTests { public void afterPropertiesSetDefaultsNullablePropertiesCorrectly() { factoryBean.setMappingContext(mappingContext); - factoryBean.setConverter(new BasicRelationalConverter(mappingContext)); + factoryBean.setConverter(new BasicJdbcConverter(mappingContext, JdbcTypeFactory.unsupported())); factoryBean.setApplicationEventPublisher(publisher); factoryBean.setBeanFactory(beanFactory); factoryBean.afterPropertiesSet(); 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 acb98c664..bc37ff8c5 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 @@ -26,12 +26,13 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; @@ -60,8 +61,9 @@ public class TestConfiguration { @Autowired(required = false) SqlSessionFactory sqlSessionFactory; @Bean - JdbcRepositoryFactory jdbcRepositoryFactory(@Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, - RelationalMappingContext context, RelationalConverter converter) { + JdbcRepositoryFactory jdbcRepositoryFactory( + @Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, + RelationalConverter converter) { return new JdbcRepositoryFactory(dataAccessStrategy, context, converter, publisher, namedParameterJdbcTemplate()); } @@ -76,14 +78,14 @@ public class TestConfiguration { } @Bean - DataAccessStrategy defaultDataAccessStrategy(@Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, - RelationalMappingContext context, RelationalConverter converter) { - return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter,template); + DataAccessStrategy defaultDataAccessStrategy( + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, RelationalMappingContext context, + JdbcConverter converter) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, template); } @Bean - JdbcMappingContext jdbcMappingContext(@Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, Optional namingStrategy, - CustomConversions conversions) { + JdbcMappingContext jdbcMappingContext(Optional namingStrategy, CustomConversions conversions) { JdbcMappingContext mappingContext = new JdbcMappingContext(namingStrategy.orElse(NamingStrategy.INSTANCE)); mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); @@ -96,7 +98,8 @@ public class TestConfiguration { } @Bean - RelationalConverter relationalConverter(RelationalMappingContext mappingContext, CustomConversions conversions) { - return new BasicJdbcConverter(mappingContext, conversions); + JdbcConverter relationalConverter(RelationalMappingContext mappingContext, CustomConversions conversions, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template) { + return new BasicJdbcConverter(mappingContext, conversions, new DefaultJdbcTypeFactory(template.getJdbcOperations())); } } diff --git a/spring-data-jdbc/src/test/resources/logback.xml b/spring-data-jdbc/src/test/resources/logback.xml index f1bfdbaf3..6da1bab09 100644 --- a/spring-data-jdbc/src/test/resources/logback.xml +++ b/spring-data-jdbc/src/test/resources/logback.xml @@ -7,8 +7,8 @@ - - + + 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 4edb29701..1699ad6c5 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 @@ -15,3 +15,6 @@ ALTER TABLE ELEMENT_NO_ID CREATE TABLE ARRAY_OWNER (ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, DIGITS VARCHAR(20) ARRAY[10] NOT NULL, MULTIDIMENSIONAL VARCHAR(20) ARRAY[10] NULL); + +CREATE TABLE BYTE_ARRAY_OWNER (ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, BINARY_DATA VARBINARY(20) NOT NULL); + diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql index 8b739ea10..38957bcc6 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql @@ -8,4 +8,6 @@ CREATE TABLE ONE_TO_ONE_PARENT ( id3 BIGINT AUTO_INCREMENT PRIMARY KEY, content CREATE TABLE Child_No_Id (ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, content VARCHAR(30)); CREATE TABLE LIST_PARENT ( id4 BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); -CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT BIGINT); \ No newline at end of file +CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT BIGINT); + +CREATE TABLE BYTE_ARRAY_OWNER (ID BIGINT AUTO_INCREMENT PRIMARY KEY, BINARY_DATA VARBINARY(20) NOT NULL) \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql index 764f92436..b507634c3 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql @@ -12,4 +12,7 @@ CREATE TABLE Child_No_Id (ONE_TO_ONE_PARENT BIGINT PRIMARY KEY, content VARCHAR( DROP TABLE IF EXISTS element_no_id; DROP TABLE IF EXISTS LIST_PARENT; CREATE TABLE LIST_PARENT ( id4 BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(100)); -CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT BIGINT); \ No newline at end of file +CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT BIGINT); + +DROP TABLE IF EXISTS BYTE_ARRAY_OWNER; +CREATE TABLE BYTE_ARRAY_OWNER (ID BIGINT IDENTITY PRIMARY KEY, BINARY_DATA VARBINARY(20) NOT NULL) \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql index 07ecf86ba..38957bcc6 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql @@ -9,3 +9,5 @@ CREATE TABLE Child_No_Id (ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, content VARCHAR CREATE TABLE LIST_PARENT ( id4 BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT BIGINT); + +CREATE TABLE BYTE_ARRAY_OWNER (ID BIGINT AUTO_INCREMENT PRIMARY KEY, BINARY_DATA VARBINARY(20) NOT NULL) \ No newline at end of file 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 d5ba24a96..4f3936234 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 @@ -1,5 +1,11 @@ DROP TABLE MANUAL; DROP TABLE LEGO_SET; +DROP TABLE ONE_TO_ONE_PARENT; +DROP TABLE Child_No_Id; +DROP TABLE LIST_PARENT; +DROP TABLE element_no_id; +DROP TABLE ARRAY_OWNER; +DROP TABLE BYTE_ARRAY_OWNER; CREATE TABLE LEGO_SET ( id1 SERIAL PRIMARY KEY, NAME VARCHAR(30)); CREATE TABLE MANUAL ( id2 SERIAL PRIMARY KEY, LEGO_SET BIGINT, ALTERNATIVE BIGINT, CONTENT VARCHAR(2000)); @@ -14,3 +20,5 @@ CREATE TABLE LIST_PARENT ( id4 SERIAL PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT INTEGER); CREATE TABLE ARRAY_OWNER (ID SERIAL PRIMARY KEY, DIGITS VARCHAR(20)[10], MULTIDIMENSIONAL VARCHAR(20)[10][10]); + +CREATE TABLE BYTE_ARRAY_OWNER (ID SERIAL PRIMARY KEY, BINARY_DATA BYTEA NOT NULL) \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql new file mode 100644 index 000000000..dd3175f7e --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql @@ -0,0 +1 @@ +CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10)) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mariadb.sql new file mode 100644 index 000000000..bbf3cfb96 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mariadb.sql @@ -0,0 +1 @@ +CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT AUTO_INCREMENT PRIMARY KEY, Stringy_number DECIMAL(20,10)) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mssql.sql new file mode 100644 index 000000000..a9e632f60 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mssql.sql @@ -0,0 +1 @@ +CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10)) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mysql.sql new file mode 100644 index 000000000..bbf3cfb96 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mysql.sql @@ -0,0 +1 @@ +CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT AUTO_INCREMENT PRIMARY KEY, Stringy_number DECIMAL(20,10)) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql new file mode 100644 index 000000000..0852a7512 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql @@ -0,0 +1 @@ +CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id SERIAL PRIMARY KEY, Stringy_number DECIMAL(20,10)) diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index 07fcf33e3..d1592ccb6 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -188,6 +188,12 @@ Converters should be annotated with `@ReadingConverter` or `@WritingConverter` i `TIMESTAMPTZ` in the example is a database specific data type that needs conversion into something more suitable for a domain model. +==== JdbcValue + +When setting bind parameters with a JDBC driver one may opt to provide a `java.sql.Types` constant value to denote the type of the parameter. +If for a value this type need to be specified this can be done by using a writing converter as described in the previous section. +This converter should convert to `JdbcValue` which has a field for the value and one of for the `JDBCType`. + [[jdbc.entity-persistence.naming-strategy]] === `NamingStrategy`