From fb858bf1b19eb4700c2dd7861e1adc059212af28 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 17 Jul 2018 15:40:51 +0200 Subject: [PATCH] DATAJDBC-235 - Add support for configurable conversion. We now support configurable conversion by introducing CustomConversions and RelationalConverter. CustomConversions is a registry for converters that should be applied on a per-type basis for properties. CustomConversions is typically registered as bean and fed into RelationalMappingContext and the newly introduced RelationalConverter to consider simple types and conversion rules. RelationalConverter with its implementation BasicRelationalConverter encapsulates conversion infrastructure such as EntityInstantiator, CustomConversions, and MappingContext that is required during relational value conversion. BasicRelationalConverter is responsible for simple value conversion and entity instantiation to pull related code together. It's not in full charge of row result to object mapping as this responsibility remains as part of DataAccessStrategy. This change supersedes and removes ConversionCustomizer. --- .../jdbc/core/DefaultDataAccessStrategy.java | 48 ++-- .../data/jdbc/core/EntityRowMapper.java | 40 +-- .../core/convert/JdbcCustomConversions.java | 59 ++++ .../jdbc/core/mapping/JdbcSimpleTypes.java | 77 ++++++ .../mybatis/MyBatisDataAccessStrategy.java | 13 +- .../repository/config/JdbcConfiguration.java | 36 ++- .../support/JdbcQueryLookupStrategy.java | 33 ++- .../support/JdbcRepositoryFactory.java | 32 +-- .../support/JdbcRepositoryFactoryBean.java | 23 +- .../conversion/BasicRelationalConverter.java | 256 ++++++++++++++++++ .../core/conversion/RelationalConverter.java | 90 ++++++ .../core/mapping/ConversionCustomizer.java | 40 --- .../mapping/RelationalMappingContext.java | 53 +--- .../DefaultDataAccessStrategyUnitTests.java | 71 ++++- .../jdbc/core/EntityRowMapperUnitTests.java | 17 +- .../model/NamingStrategyUnitTests.java | 7 +- .../mybatis/MyBatisHsqlIntegrationTests.java | 8 +- .../SimpleJdbcRepositoryEventsUnitTests.java | 10 +- .../JdbcQueryLookupStrategyUnitTests.java | 9 +- .../JdbcRepositoryFactoryBeanUnitTests.java | 4 + .../data/jdbc/testing/TestConfiguration.java | 42 ++- .../BasicRelationalConverterUnitTests.java | 104 +++++++ 22 files changed, 833 insertions(+), 239 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/mapping/JdbcSimpleTypes.java create mode 100644 src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java create mode 100644 src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java delete mode 100644 src/main/java/org/springframework/data/relational/core/mapping/ConversionCustomizer.java create mode 100644 src/test/java/org/springframework/data/relational/core/conversion/BasicRelationalConverterUnitTests.java diff --git a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index 3ae56f702..ac528ad07 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -27,15 +27,15 @@ import java.util.stream.StreamSupport; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.NonTransientDataAccessException; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +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.util.ClassTypeInformation; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -48,6 +48,7 @@ import org.springframework.util.Assert; * The default {@link DataAccessStrategy} is to generate SQL statements based on meta data from the entity. * * @author Jens Schauder + * @author Mark Paluch * @since 1.0 */ @RequiredArgsConstructor @@ -59,8 +60,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { private final @NonNull SqlGeneratorSource sqlGeneratorSource; private final @NonNull RelationalMappingContext context; + private final @NonNull RelationalConverter converter; private final @NonNull NamedParameterJdbcOperations operations; - private final @NonNull EntityInstantiators instantiators; private final @NonNull DataAccessStrategy accessStrategy; /** @@ -68,12 +69,12 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { * Only suitable if this is the only access strategy in use. */ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, - NamedParameterJdbcOperations operations, EntityInstantiators instantiators) { + RelationalConverter converter, NamedParameterJdbcOperations operations) { this.sqlGeneratorSource = sqlGeneratorSource; this.operations = operations; this.context = context; - this.instantiators = instantiators; + this.converter = converter; this.accessStrategy = this; } @@ -92,7 +93,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { Assert.notNull(idProperty, "Since we have a non-null idValue, we must have an idProperty as well."); - additionalParameters.put(idProperty.getColumnName(), convert(idValue, idProperty.getColumnType())); + additionalParameters.put(idProperty.getColumnName(), + converter.writeValue(idValue, ClassTypeInformation.from(idProperty.getColumnType()))); } additionalParameters.forEach(parameterSource::addValue); @@ -231,7 +233,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { MapSqlParameterSource parameter = new MapSqlParameterSource( // "ids", // StreamSupport.stream(ids.spliterator(), false) // - .map(id -> convert(id, targetType)) // + .map(id -> converter.writeValue(id, ClassTypeInformation.from(targetType))) // .collect(Collectors.toList()) // ); @@ -281,11 +283,15 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { MapSqlParameterSource parameters = new MapSqlParameterSource(); + PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(instance); + persistentEntity.doWithProperties((PropertyHandler) property -> { + if (!property.isEntity()) { - Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property); - Object convertedValue = convert(value, property.getColumnType()); + Object value = propertyAccessor.getProperty(property); + + Object convertedValue = converter.writeValue(value, ClassTypeInformation.from(property.getColumnType())); parameters.addValue(property.getColumnName(), convertedValue, JdbcUtil.sqlTypeFor(property.getColumnType())); } }); @@ -318,12 +324,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { getIdFromHolder(holder, persistentEntity).ifPresent(it -> { - PersistentPropertyAccessor accessor = persistentEntity.getPropertyAccessor(instance); - ConvertingPropertyAccessor convertingPropertyAccessor = new ConvertingPropertyAccessor(accessor, - context.getConversions()); + PersistentPropertyAccessor accessor = converter.getPropertyAccessor(persistentEntity, instance); RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); - convertingPropertyAccessor.setProperty(idProperty, it); + accessor.setProperty(idProperty, it); }); } catch (NonTransientDataAccessException e) { @@ -344,7 +348,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { } public EntityRowMapper getEntityRowMapper(Class domainType) { - return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context, instantiators, accessStrategy); + return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context, converter, accessStrategy); } @SuppressWarnings("unchecked") @@ -360,7 +364,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { Class columnType = getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType(); - return new MapSqlParameterSource("id", convert(id, columnType)); + return new MapSqlParameterSource("id", converter.writeValue(id, ClassTypeInformation.from(columnType))); } @SuppressWarnings("unchecked") @@ -368,20 +372,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { return (RelationalPersistentEntity) context.getRequiredPersistentEntity(domainType); } - @Nullable - private V convert(@Nullable Object from, Class to) { - - if (from == null) { - return null; - } - - RelationalPersistentEntity persistentEntity = context.getPersistentEntity(from.getClass()); - - Object id = persistentEntity == null ? null : persistentEntity.getIdentifierAccessor(from).getIdentifier(); - - return context.getConversions().convert(id == null ? from : id, to); - } - private SqlGenerator sql(Class domainType) { return sqlGeneratorSource.getSqlGenerator(domainType); } diff --git a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java index 152fd2f78..b06878a8a 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -22,15 +22,13 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; -import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor.Parameter; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ParameterValueProvider; +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; @@ -39,9 +37,8 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Maps a {@link ResultSet} to an entity of type {@code T}, including entities referenced. - * - * This {@link RowMapper} might trigger additional SQL statements in order to load other members of the same aggregate. + * Maps a {@link ResultSet} to an entity of type {@code T}, including entities referenced. This {@link RowMapper} might + * trigger additional SQL statements in order to load other members of the same aggregate. * * @author Jens Schauder * @author Oliver Gierke @@ -54,19 +51,17 @@ public class EntityRowMapper implements RowMapper { private final RelationalPersistentEntity entity; - private final ConversionService conversions; + private final RelationalConverter converter; private final RelationalMappingContext context; private final DataAccessStrategy accessStrategy; private final RelationalPersistentProperty idProperty; - private final EntityInstantiators instantiators; - public EntityRowMapper(RelationalPersistentEntity entity, RelationalMappingContext context, EntityInstantiators instantiators, - DataAccessStrategy accessStrategy) { + public EntityRowMapper(RelationalPersistentEntity entity, RelationalMappingContext context, + RelationalConverter converter, DataAccessStrategy accessStrategy) { this.entity = entity; - this.conversions = context.getConversions(); + this.converter = converter; this.context = context; - this.instantiators = instantiators; this.accessStrategy = accessStrategy; this.idProperty = entity.getIdProperty(); } @@ -80,8 +75,7 @@ public class EntityRowMapper implements RowMapper { T result = createInstance(entity, resultSet, ""); - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(result), - conversions); + PersistentPropertyAccessor propertyAccessor = converter.getPropertyAccessor(entity, result); Object id = idProperty == null ? null : readFrom(resultSet, idProperty, ""); @@ -118,7 +112,7 @@ public class EntityRowMapper implements RowMapper { return readEntityFrom(resultSet, property); } - return resultSet.getObject(prefix + property.getColumnName()); + return converter.readValue(resultSet.getObject(prefix + property.getColumnName()), property.getTypeInformation()); } catch (SQLException o_O) { throw new MappingException(String.format("Could not read property %s from result set!", property), o_O); @@ -138,23 +132,19 @@ public class EntityRowMapper implements RowMapper { return null; } - S instance = - createInstance(entity, rs, prefix); + S instance = createInstance(entity, rs, prefix); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); + PersistentPropertyAccessor accessor = converter.getPropertyAccessor(entity, instance); for (RelationalPersistentProperty p : entity) { - propertyAccessor.setProperty(p, readFrom(rs, p, prefix)); + accessor.setProperty(p, readFrom(rs, p, prefix)); } return instance; } private S createInstance(RelationalPersistentEntity entity, ResultSet rs, String prefix) { - - return instantiators.getInstantiatorFor(entity) // - .createInstance(entity, new ResultSetParameterValueProvider(rs, entity, conversions, prefix)); + return converter.createInstance(entity, new ResultSetParameterValueProvider(rs, entity, prefix)); } @RequiredArgsConstructor @@ -162,13 +152,13 @@ public class EntityRowMapper implements RowMapper { @NonNull private final ResultSet resultSet; @NonNull private final RelationalPersistentEntity entity; - @NonNull private final ConversionService conversionService; @NonNull private final String prefix; /* * (non-Javadoc) * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter) */ + @SuppressWarnings("unchecked") @Override public T getParameterValue(Parameter parameter) { @@ -177,7 +167,7 @@ public class EntityRowMapper implements RowMapper { String column = prefix + entity.getRequiredPersistentProperty(parameterName).getColumnName(); try { - return conversionService.convert(resultSet.getObject(column), parameter.getType().getType()); + return (T) resultSet.getObject(column); } catch (SQLException o_O) { throw new MappingException(String.format("Couldn't read column %s from ResultSet.", column), o_O); } diff --git a/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java b/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java new file mode 100644 index 000000000..8d241618f --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java @@ -0,0 +1,59 @@ +/* + * Copyright 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.core.convert; + +import java.util.Collections; +import java.util.List; + +import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; + +/** + * Value object to capture custom conversion. {@link JdbcCustomConversions} also act as factory for + * {@link org.springframework.data.mapping.model.SimpleTypeHolder} + * + * @author Mark Paluch + * @see org.springframework.data.convert.CustomConversions + * @see org.springframework.data.mapping.model.SimpleTypeHolder + * @see JdbcSimpleTypes + */ +public class JdbcCustomConversions extends org.springframework.data.convert.CustomConversions { + + private static final StoreConversions STORE_CONVERSIONS; + private static final List STORE_CONVERTERS; + + static { + + STORE_CONVERTERS = Collections.emptyList(); + STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER, STORE_CONVERTERS); + } + + /** + * Creates an empty {@link JdbcCustomConversions} object. + */ + public JdbcCustomConversions() { + this(Collections.emptyList()); + } + + /** + * Create a new {@link JdbcCustomConversions} instance registering the given converters. + * + * @param converters must not be {@literal null}. + */ + public JdbcCustomConversions(List converters) { + super(STORE_CONVERSIONS, converters); + } + +} diff --git a/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcSimpleTypes.java b/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcSimpleTypes.java new file mode 100644 index 000000000..7c0d7386c --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcSimpleTypes.java @@ -0,0 +1,77 @@ +/* + * Copyright 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.core.mapping; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.RowId; +import java.sql.Struct; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * Simple constant holder for a {@link SimpleTypeHolder} enriched with specific simple types for relational database + * access. + * + * @author Mark Paluch + */ +public abstract class JdbcSimpleTypes { + + public static final Set> AUTOGENERATED_ID_TYPES; + + static { + + Set> classes = new HashSet<>(); + classes.add(Long.class); + classes.add(String.class); + classes.add(BigInteger.class); + classes.add(BigDecimal.class); + classes.add(UUID.class); + AUTOGENERATED_ID_TYPES = Collections.unmodifiableSet(classes); + + Set> simpleTypes = new HashSet<>(); + simpleTypes.add(BigDecimal.class); + simpleTypes.add(BigInteger.class); + simpleTypes.add(Array.class); + simpleTypes.add(Clob.class); + simpleTypes.add(Blob.class); + simpleTypes.add(java.sql.Date.class); + simpleTypes.add(NClob.class); + simpleTypes.add(Ref.class); + simpleTypes.add(RowId.class); + simpleTypes.add(Struct.class); + simpleTypes.add(Time.class); + simpleTypes.add(Timestamp.class); + + JDBC_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + } + + private static final Set> JDBC_SIMPLE_TYPES; + public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(JDBC_SIMPLE_TYPES, true); + + private JdbcSimpleTypes() {} +} diff --git a/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 08b36769a..51ea83981 100644 --- a/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -29,6 +29,7 @@ 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.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.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -40,12 +41,13 @@ import org.springframework.util.Assert; * "Mapper". This is then followed by the method name separated by a dot. For methods taking a {@link PropertyPath} as * argument, the relevant entity is that of the root of the path, and the path itself gets as dot separated String * appended to the statement name. Each statement gets an instance of {@link MyBatisContext}, which at least has the - * entityType set. For methods taking a {@link PropertyPath} the entityTyoe if the context is set to the class of the + * entityType set. For methods taking a {@link PropertyPath} the entityType if the context is set to the class of the * leaf type. * * @author Jens Schauder * @author Kazuki Shimizu * @author Oliver Gierke + * @author Mark Paluch * @since 1.0 */ public class MyBatisDataAccessStrategy implements DataAccessStrategy { @@ -58,8 +60,9 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { * uses a {@link DefaultDataAccessStrategy} */ public static DataAccessStrategy createCombinedAccessStrategy(RelationalMappingContext context, + RelationalConverter converter, NamedParameterJdbcOperations operations, SqlSession sqlSession) { - return createCombinedAccessStrategy(context, new EntityInstantiators(), operations, sqlSession, + return createCombinedAccessStrategy(context, converter, operations, sqlSession, NamespaceStrategy.DEFAULT_INSTANCE); } @@ -68,7 +71,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { * uses a {@link DefaultDataAccessStrategy} */ public static DataAccessStrategy createCombinedAccessStrategy(RelationalMappingContext context, - EntityInstantiators instantiators, NamedParameterJdbcOperations operations, SqlSession sqlSession, + RelationalConverter converter, NamedParameterJdbcOperations operations, SqlSession sqlSession, NamespaceStrategy namespaceStrategy) { // the DefaultDataAccessStrategy needs a reference to the returned DataAccessStrategy. This creates a dependency @@ -85,8 +88,8 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { DefaultDataAccessStrategy defaultDataAccessStrategy = new DefaultDataAccessStrategy( // sqlGeneratorSource, // context, // + converter, // operations, // - instantiators, // cascadingDataAccessStrategy // ); @@ -113,7 +116,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { /** * Set a NamespaceStrategy to be used. - * + * * @param namespaceStrategy Must be non {@literal null} */ public void setNamespaceStrategy(NamespaceStrategy namespaceStrategy) { diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java index 82171e6df..17a530387 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java +++ b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java @@ -19,15 +19,20 @@ import java.util.Optional; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.relational.core.mapping.ConversionCustomizer; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.relational.core.conversion.BasicRelationalConverter; +import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** * Beans that must be registered for Spring Data JDBC to work. * * @author Greg Turnquist * @author Jens Schauder + * @author Mark Paluch * @since 1.0 */ @Configuration @@ -35,9 +40,30 @@ public class JdbcConfiguration { @Bean RelationalMappingContext jdbcMappingContext(Optional namingStrategy, - Optional conversionCustomizer) { + CustomConversions customConversions) { + + RelationalMappingContext mappingContext = new RelationalMappingContext( + namingStrategy.orElse(NamingStrategy.INSTANCE)); + mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); + + return mappingContext; + } - return new RelationalMappingContext(namingStrategy.orElse(NamingStrategy.INSTANCE), - conversionCustomizer.orElse(ConversionCustomizer.NONE)); + @Bean + RelationalConverter relationalConverter(RelationalMappingContext mappingContext, + CustomConversions customConversions) { + return new BasicRelationalConverter(mappingContext, customConversions); + } + + /** + * Register custom {@link Converter}s in a {@link CustomConversions} object if required. These + * {@link CustomConversions} will be registered with the {@link #jdbcMappingContext()}. Returns an empty + * {@link JdbcCustomConversions} instance by default. + * + * @return must not be {@literal null}. + */ + @Bean + CustomConversions jdbcCustomConversions() { + return new JdbcCustomConversions(); } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index b4d88fb86..c05d2dc02 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -17,13 +17,13 @@ package org.springframework.data.jdbc.repository.support; import java.lang.reflect.Method; -import org.springframework.core.convert.ConversionService; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.EntityRowMapper; import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.projection.ProjectionFactory; +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.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; @@ -44,33 +44,32 @@ import org.springframework.util.Assert; class JdbcQueryLookupStrategy implements QueryLookupStrategy { private final RelationalMappingContext context; - private final EntityInstantiators instantiators; + private final RelationalConverter converter; private final DataAccessStrategy accessStrategy; private final RowMapperMap rowMapperMap; private final NamedParameterJdbcOperations operations; - private final ConversionService conversionService; - /** - * Creates a new {@link JdbcQueryLookupStrategy} for the given {@link RelationalMappingContext}, {@link DataAccessStrategy} - * and {@link RowMapperMap}. + * Creates a new {@link JdbcQueryLookupStrategy} for the given {@link RelationalMappingContext}, + * {@link DataAccessStrategy} and {@link RowMapperMap}. * * @param context must not be {@literal null}. + * @param converter must not be {@literal null}. * @param accessStrategy must not be {@literal null}. * @param rowMapperMap must not be {@literal null}. */ - JdbcQueryLookupStrategy(RelationalMappingContext context, EntityInstantiators instantiators, + JdbcQueryLookupStrategy(RelationalMappingContext context, RelationalConverter converter, DataAccessStrategy accessStrategy, RowMapperMap rowMapperMap, NamedParameterJdbcOperations operations) { - Assert.notNull(context, "JdbcMappingContext must not be null!"); + Assert.notNull(context, "RelationalMappingContext must not be null!"); + Assert.notNull(converter, "RelationalConverter must not be null!"); Assert.notNull(accessStrategy, "DataAccessStrategy must not be null!"); Assert.notNull(rowMapperMap, "RowMapperMap must not be null!"); this.context = context; - this.instantiators = instantiators; + this.converter = converter; this.accessStrategy = accessStrategy; this.rowMapperMap = rowMapperMap; - this.conversionService = context.getConversions(); this.operations = operations; } @@ -93,9 +92,13 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { Class returnedObjectType = queryMethod.getReturnedObjectType(); - return context.getSimpleTypeHolder().isSimpleType(returnedObjectType) - ? SingleColumnRowMapper.newInstance(returnedObjectType, conversionService) - : determineDefaultRowMapper(queryMethod); + RelationalPersistentEntity persistentEntity = context.getPersistentEntity(returnedObjectType); + + if (persistentEntity == null) { + return SingleColumnRowMapper.newInstance(returnedObjectType, converter.getConversionService()); + } + + return determineDefaultRowMapper(queryMethod); } private RowMapper determineDefaultRowMapper(JdbcQueryMethod queryMethod) { @@ -108,7 +111,7 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { ? new EntityRowMapper<>( // context.getRequiredPersistentEntity(domainType), // context, // - instantiators, // + converter, // accessStrategy) // : typeMappedRowMapper; } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index 539a1cb12..9a72eb449 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -18,10 +18,10 @@ package org.springframework.data.jdbc.repository.support; import java.util.Optional; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; 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.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.repository.core.EntityInformation; @@ -41,36 +41,40 @@ import org.springframework.util.Assert; * @author Jens Schauder * @author Greg Turnquist * @author Christoph Strobl + * @author Mark Paluch * @since 1.0 */ public class JdbcRepositoryFactory extends RepositoryFactorySupport { private final RelationalMappingContext context; + private final RelationalConverter converter; private final ApplicationEventPublisher publisher; private final DataAccessStrategy accessStrategy; private final NamedParameterJdbcOperations operations; private RowMapperMap rowMapperMap = RowMapperMap.EMPTY; - private EntityInstantiators instantiators = new EntityInstantiators(); /** - * Creates a new {@link JdbcRepositoryFactory} for the given {@link DataAccessStrategy}, {@link RelationalMappingContext} - * and {@link ApplicationEventPublisher}. - * + * Creates a new {@link JdbcRepositoryFactory} for the given {@link DataAccessStrategy}, + * {@link RelationalMappingContext} and {@link ApplicationEventPublisher}. + * * @param dataAccessStrategy must not be {@literal null}. * @param context must not be {@literal null}. + * @param converter must not be {@literal null}. * @param publisher must not be {@literal null}. * @param operations must not be {@literal null}. */ public JdbcRepositoryFactory(DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, - ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations) { + RelationalConverter converter, ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations) { Assert.notNull(dataAccessStrategy, "DataAccessStrategy must not be null!"); - Assert.notNull(context, "JdbcMappingContext must not be null!"); + Assert.notNull(context, "RelationalMappingContext must not be null!"); + Assert.notNull(converter, "RelationalConverter must not be null!"); Assert.notNull(publisher, "ApplicationEventPublisher must not be null!"); this.publisher = publisher; this.context = context; + this.converter = converter; this.accessStrategy = dataAccessStrategy; this.operations = operations; } @@ -85,18 +89,6 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { this.rowMapperMap = rowMapperMap; } - /** - * Set the {@link EntityInstantiators} used for instantiating entity instances. - * - * @param instantiators Must not be {@code null}. - */ - public void setEntityInstantiators(EntityInstantiators instantiators) { - - Assert.notNull(instantiators, "EntityInstantiators must not be null."); - - this.instantiators = instantiators; - } - @SuppressWarnings("unchecked") @Override public EntityInformation getEntityInformation(Class aClass) { @@ -146,6 +138,6 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); } - return Optional.of(new JdbcQueryLookupStrategy(context, instantiators, accessStrategy, rowMapperMap, operations)); + return Optional.of(new JdbcQueryLookupStrategy(context, converter, accessStrategy, rowMapperMap, operations)); } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index 97351390a..cd44d1283 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -20,11 +20,11 @@ import java.io.Serializable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.data.convert.EntityInstantiators; 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.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; @@ -47,14 +47,14 @@ public class JdbcRepositoryFactoryBean, S, ID extend private ApplicationEventPublisher publisher; private RelationalMappingContext mappingContext; + private RelationalConverter converter; private DataAccessStrategy dataAccessStrategy; private RowMapperMap rowMapperMap = RowMapperMap.EMPTY; private NamedParameterJdbcOperations operations; - private EntityInstantiators instantiators = new EntityInstantiators(); /** * Creates a new {@link JdbcRepositoryFactoryBean} for the given repository interface. - * + * * @param repositoryInterface must not be {@literal null}. */ JdbcRepositoryFactoryBean(Class repositoryInterface) { @@ -80,7 +80,7 @@ public class JdbcRepositoryFactoryBean, S, ID extend protected RepositoryFactorySupport doCreateRepositoryFactory() { JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(dataAccessStrategy, mappingContext, - publisher, operations); + converter, publisher, operations); jdbcRepositoryFactory.setRowMapperMap(rowMapperMap); return jdbcRepositoryFactory; @@ -115,9 +115,9 @@ public class JdbcRepositoryFactoryBean, S, ID extend this.operations = operations; } - @Autowired(required = false) - public void setInstantiators(EntityInstantiators instantiators) { - this.instantiators = instantiators; + @Autowired + public void setConverter(RelationalConverter converter) { + this.converter = converter; } /* @@ -128,22 +128,19 @@ public class JdbcRepositoryFactoryBean, S, ID extend public void afterPropertiesSet() { Assert.state(this.mappingContext != null, "MappingContext is required and must not be null!"); + Assert.state(this.converter != null, "RelationalConverter is required and must not be null!"); if (dataAccessStrategy == null) { SqlGeneratorSource sqlGeneratorSource = new SqlGeneratorSource(mappingContext); - this.dataAccessStrategy = new DefaultDataAccessStrategy(sqlGeneratorSource, mappingContext, operations, - instantiators); + this.dataAccessStrategy = new DefaultDataAccessStrategy(sqlGeneratorSource, mappingContext, converter, + operations); } if (rowMapperMap == null) { this.rowMapperMap = RowMapperMap.EMPTY; } - if (instantiators == null) { - this.instantiators = new EntityInstantiators(); - } - super.afterPropertiesSet(); } } diff --git a/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java b/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java new file mode 100644 index 000000000..8b3fc9514 --- /dev/null +++ b/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java @@ -0,0 +1,256 @@ +/* + * Copyright 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.relational.core.conversion; + +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.Optional; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.CustomConversions.StoreConversions; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link RelationalConverter} that uses a {@link MappingContext} to apply basic conversion of relational values to + * property values. + *

+ * Conversion is configurable by providing a customized {@link CustomConversions}. + * + * @author Mark Paluch + * @see MappingContext + * @see SimpleTypeHolder + * @see CustomConversions + */ +public class BasicRelationalConverter implements RelationalConverter { + + private final MappingContext, RelationalPersistentProperty> context; + private final ConfigurableConversionService conversionService; + private final EntityInstantiators entityInstantiators; + private final CustomConversions conversions; + + /** + * Creates a new {@link BasicRelationalConverter} given {@link MappingContext}. + * + * @param context must not be {@literal null}. org.springframework.data.jdbc.core.DefaultDataAccessStrategyUnitTests + */ + public BasicRelationalConverter( + MappingContext, ? extends RelationalPersistentProperty> context) { + this(context, new CustomConversions(StoreConversions.NONE, Collections.emptyList()), new DefaultConversionService(), + new EntityInstantiators()); + } + + /** + * 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}. + */ + public BasicRelationalConverter( + MappingContext, ? extends RelationalPersistentProperty> context, + CustomConversions conversions) { + this(context, conversions, new DefaultConversionService(), new EntityInstantiators()); + } + + @SuppressWarnings("unchecked") + private BasicRelationalConverter( + MappingContext, ? extends RelationalPersistentProperty> context, + CustomConversions conversions, ConfigurableConversionService conversionService, + EntityInstantiators entityInstantiators) { + + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(conversions, "CustomConversions must not be null!"); + + this.context = (MappingContext) context; + this.conversionService = conversionService; + this.entityInstantiators = entityInstantiators; + this.conversions = conversions; + + conversions.registerConvertersIn(this.conversionService); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.conversion.RelationalConverter#getConversionService() + */ + @Override + public ConversionService getConversionService() { + return conversionService; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.conversion.RelationalConverter#getMappingContext() + */ + @Override + public MappingContext, ? extends RelationalPersistentProperty> getMappingContext() { + return context; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.conversion.RelationalConverter#getPropertyAccessor(org.springframework.data.mapping.PersistentEntity, java.lang.Object) + */ + @Override + public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity persistentEntity, T instance) { + + PersistentPropertyAccessor accessor = persistentEntity.getPropertyAccessor(instance); + return new ConvertingPropertyAccessor<>(accessor, conversionService); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.conversion.RelationalConverter#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider) + */ + @Override + public T createInstance(PersistentEntity entity, + ParameterValueProvider parameterValueProvider) { + + return entityInstantiators.getInstantiatorFor(entity) // + .createInstance(entity, new ConvertingParameterValueProvider<>(parameterValueProvider)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.conversion.RelationalConverter#readValue(java.lang.Object, org.springframework.data.util.TypeInformation) + */ + @Override + @Nullable + public Object readValue(@Nullable Object value, TypeInformation type) { + + if (null == value) { + return null; + } + + if (conversions.hasCustomReadTarget(value.getClass(), type.getType())) { + return conversionService.convert(value, type.getType()); + } else { + return getPotentiallyConvertedSimpleRead(value, type.getType()); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.conversion.RelationalConverter#writeValue(java.lang.Object, org.springframework.data.util.TypeInformation) + */ + @Override + @Nullable + public Object writeValue(@Nullable Object value, TypeInformation type) { + + if (value == null) { + return null; + } + + Class rawType = type.getType(); + RelationalPersistentEntity persistentEntity = context.getPersistentEntity(value.getClass()); + + if (persistentEntity != null) { + + Object id = persistentEntity.getIdentifierAccessor(value).getIdentifier(); + return writeValue(id, type); + } + + if (rawType.isInstance(value)) { + return getPotentiallyConvertedSimpleWrite(value); + } + + return conversionService.convert(value, rawType); + } + + /** + * Checks whether we have a custom conversion registered for the given value into an arbitrary simple JDBC type. + * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. + * + * @param value + * @return + */ + private Object getPotentiallyConvertedSimpleWrite(Object value) { + + Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); + + if (customTarget.isPresent()) { + return conversionService.convert(value, customTarget.get()); + } + + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; + } + + /** + * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies + * {@link Enum} handling or returns the value as is. + * + * @param value + * @param target must not be {@literal null}. + * @return + */ + @Nullable + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { + + if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { + return value; + } + + if (conversions.hasCustomReadTarget(value.getClass(), target)) { + return conversionService.convert(value, target); + } + + if (Enum.class.isAssignableFrom(target)) { + return Enum.valueOf((Class) target, value.toString()); + } + + return conversionService.convert(value, target); + } + + /** + * Converter-aware {@link ParameterValueProvider}. + * + * @param

+ * @author Mark Paluch + */ + @RequiredArgsConstructor + class ConvertingParameterValueProvider

> implements ParameterValueProvider

{ + + private final ParameterValueProvider

delegate; + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Override + @SuppressWarnings("unchecked") + public T getParameterValue(Parameter parameter) { + return (T) readValue(delegate.getParameterValue(parameter), parameter.getType()); + } + } +} diff --git a/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java b/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java new file mode 100644 index 000000000..19aa676d1 --- /dev/null +++ b/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java @@ -0,0 +1,90 @@ +/* + * Copyright 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.relational.core.conversion; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * A {@link RelationalConverter} is responsible for converting for values to the native relational representation and + * vice versa. + * + * @author Mark Paluch + */ +public interface RelationalConverter { + + /** + * Returns the underlying {@link MappingContext} used by the converter. + * + * @return never {@literal null} + */ + MappingContext, ? extends RelationalPersistentProperty> getMappingContext(); + + /** + * Returns the underlying {@link ConversionService} used by the converter. + * + * @return never {@literal null}. + */ + ConversionService getConversionService(); + + /** + * Create a new instance of {@link PersistentEntity} given {@link ParameterValueProvider} to obtain constructor + * properties. + * + * @param entity + * @param parameterValueProvider + * @param + * @return + */ + T createInstance(PersistentEntity entity, + ParameterValueProvider parameterValueProvider); + + /** + * Return a {@link PersistentPropertyAccessor} to access property values of the {@code instance}. + * + * @param persistentEntity + * @param instance + * @return + */ + PersistentPropertyAccessor getPropertyAccessor(PersistentEntity persistentEntity, T instance); + + /** + * Read a relational value into the desired {@link TypeInformation destination type}. + * + * @param value + * @param type + * @return + */ + @Nullable + Object readValue(@Nullable Object value, TypeInformation type); + + /** + * Write a property value into a relational type that can be stored natively. + * + * @param value + * @param type + * @return + */ + @Nullable + Object writeValue(@Nullable Object value, TypeInformation type); +} diff --git a/src/main/java/org/springframework/data/relational/core/mapping/ConversionCustomizer.java b/src/main/java/org/springframework/data/relational/core/mapping/ConversionCustomizer.java deleted file mode 100644 index 92679eb2f..000000000 --- a/src/main/java/org/springframework/data/relational/core/mapping/ConversionCustomizer.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.relational.core.mapping; - -import org.springframework.core.convert.support.GenericConversionService; - -/** - * Interface to register custom conversions. - * - * @author Jens Schauder - * @since 1.0 - */ -public interface ConversionCustomizer { - - /** - * Noop instance to be used as a default. - */ - ConversionCustomizer NONE = __ -> {}; - - /** - * Gets called in order to allow the customization of the {@link org.springframework.core.convert.ConversionService}. - * Typically used by registering additional conversions. - * - * @param conversions the conversions that get customized. - */ - void customize(GenericConversionService conversions); -} diff --git a/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index 78a2bb6ec..76c56efec 100644 --- a/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -15,22 +15,12 @@ */ package org.springframework.data.relational.core.mapping; -import static java.util.Arrays.*; - import lombok.Getter; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.time.temporal.Temporal; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.data.convert.Jsr310Converters; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.context.MappingContext; @@ -47,60 +37,33 @@ import org.springframework.util.Assert; * @author Greg Turnquist * @author Kazuki Shimizu * @author Oliver Gierke + * @author Mark Paluch * @since 1.0 */ public class RelationalMappingContext extends AbstractMappingContext, RelationalPersistentProperty> { - private static final HashSet> CUSTOM_SIMPLE_TYPES = new HashSet<>(asList( // - BigDecimal.class, // - BigInteger.class, // - Temporal.class // - )); - @Getter private final NamingStrategy namingStrategy; - private final GenericConversionService conversions = getDefaultConversionService(); - @Getter private SimpleTypeHolder simpleTypeHolder; /** * Creates a new {@link RelationalMappingContext}. */ public RelationalMappingContext() { - this(NamingStrategy.INSTANCE, ConversionCustomizer.NONE); - } - - public RelationalMappingContext(NamingStrategy namingStrategy) { - this(namingStrategy, ConversionCustomizer.NONE); + this(NamingStrategy.INSTANCE); } /** - * Creates a new {@link RelationalMappingContext} using the given {@link NamingStrategy} and {@link ConversionCustomizer}. - * + * Creates a new {@link RelationalMappingContext} using the given {@link NamingStrategy}. + * * @param namingStrategy must not be {@literal null}. * @param customizer must not be {@literal null}. */ - public RelationalMappingContext(NamingStrategy namingStrategy, ConversionCustomizer customizer) { + public RelationalMappingContext(NamingStrategy namingStrategy) { Assert.notNull(namingStrategy, "NamingStrategy must not be null!"); - Assert.notNull(customizer, "ConversionCustomizer must not be null!"); this.namingStrategy = namingStrategy; - customizer.customize(conversions); - setSimpleTypeHolder(new SimpleTypeHolder(CUSTOM_SIMPLE_TYPES, true)); - } - - private static GenericConversionService getDefaultConversionService() { - - DefaultConversionService conversionService = new DefaultConversionService(); - Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter); - - return conversionService; - } - - @Override - public void setSimpleTypeHolder(SimpleTypeHolder simpleTypes) { - super.setSimpleTypeHolder(simpleTypes); - this.simpleTypeHolder = simpleTypes; + setSimpleTypeHolder(new SimpleTypeHolder(Collections.emptySet(), true)); } /** @@ -147,8 +110,4 @@ public class RelationalMappingContext extends AbstractMappingContext additionalParameters = new HashMap<>(); ArgumentCaptor paramSourceCaptor = ArgumentCaptor.forClass(SqlParameterSource.class); DefaultDataAccessStrategy accessStrategy = new DefaultDataAccessStrategy( // new SqlGeneratorSource(context), // context, // - jdbcOperations, // - new EntityInstantiators() // - ); + converter, // + jdbcOperations); @Test // DATAJDBC-146 public void additionalParameterForIdDoesNotLeadToDuplicateParameters() { @@ -83,10 +91,65 @@ public class DefaultDataAccessStrategyUnitTests { assertThat(paramSourceCaptor.getValue().getValue("id")).isEqualTo(ORIGINAL_ID); } + @Test // DATAJDBC-235 + public void considersConfiguredWriteConverter() { + + RelationalConverter converter = new BasicRelationalConverter(context, + new JdbcCustomConversions(Arrays.asList(BooleanToStringConverter.INSTANCE, StringToBooleanConverter.INSTANCE))); + + DefaultDataAccessStrategy accessStrategy = new DefaultDataAccessStrategy( // + new SqlGeneratorSource(context), // + context, // + converter, // + jdbcOperations); + + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + + EntityWithBoolean entity = new EntityWithBoolean(ORIGINAL_ID, true); + + accessStrategy.insert(entity, EntityWithBoolean.class, new HashMap<>()); + + verify(jdbcOperations).update(sqlCaptor.capture(), paramSourceCaptor.capture(), any(KeyHolder.class)); + + assertThat(sqlCaptor.getValue()) // + .contains("INSERT INTO entity_with_boolean (flag, id) VALUES (:flag, :id)"); + assertThat(paramSourceCaptor.getValue().getValue("id")).isEqualTo(ORIGINAL_ID); + assertThat(paramSourceCaptor.getValue().getValue("flag")).isEqualTo("T"); + } + @RequiredArgsConstructor private static class DummyEntity { @Id private final Long id; } + @AllArgsConstructor + private static class EntityWithBoolean { + + @Id Long id; + boolean flag; + } + + @WritingConverter + enum BooleanToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Boolean source) { + return source != null && source ? "T" : "F"; + } + } + + @ReadingConverter + enum StringToBooleanConverter implements Converter { + + INSTANCE; + + @Override + public Boolean convert(String source) { + return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE; + } + } + } diff --git a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java index c1b14e0eb..ac77d8817 100644 --- a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java @@ -38,21 +38,21 @@ import javax.naming.OperationNotSupportedException; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.annotation.Id; -import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.convert.Jsr310Converters; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.relational.core.conversion.BasicRelationalConverter; +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.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; -import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.util.Assert; /** * Tests the extraction of entities from a {@link ResultSet} by the {@link EntityRowMapper}. * * @author Jens Schauder + * @author Mark Paluch */ public class EntityRowMapperUnitTests { @@ -195,15 +195,12 @@ public class EntityRowMapperUnitTests { new SimpleEntry<>(2, new Trivial()) // ))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_LIST), any(RelationalPersistentProperty.class)); - GenericConversionService conversionService = new GenericConversionService(); - conversionService.addConverter(new IterableOfEntryToMapConverter()); - DefaultConversionService.addDefaultConverters(conversionService); - Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter); + RelationalConverter converter = new BasicRelationalConverter(context, new JdbcCustomConversions()); return new EntityRowMapper<>( // (RelationalPersistentEntity) context.getRequiredPersistentEntity(type), // context, // - new EntityInstantiators(), // + converter, // accessStrategy // ); } diff --git a/src/test/java/org/springframework/data/jdbc/mapping/model/NamingStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/mapping/model/NamingStrategyUnitTests.java index 9cee2c028..35a5dc3e6 100644 --- a/src/test/java/org/springframework/data/jdbc/mapping/model/NamingStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/mapping/model/NamingStrategyUnitTests.java @@ -16,7 +16,6 @@ package org.springframework.data.jdbc.mapping.model; import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; import lombok.Data; @@ -25,23 +24,23 @@ import java.util.List; import org.junit.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.relational.core.mapping.ConversionCustomizer; +import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.core.mapping.NamingStrategy; /** * Unit tests for the default {@link NamingStrategy}. * * @author Kazuki Shimizu * @author Jens Schauder + * @author Mark Paluch */ public class NamingStrategyUnitTests { private final NamingStrategy target = NamingStrategy.INSTANCE; private final RelationalPersistentEntity persistentEntity = // - new RelationalMappingContext(target, mock(ConversionCustomizer.class)).getRequiredPersistentEntity(DummyEntity.class); + new RelationalMappingContext(target).getRequiredPersistentEntity(DummyEntity.class); @Test // DATAJDBC-184 public void getTableName() { diff --git a/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java index 0373911e7..b9d24bf78 100644 --- a/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java @@ -33,6 +33,7 @@ import org.springframework.context.annotation.Import; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.CrudRepository; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -48,6 +49,7 @@ import org.springframework.transaction.annotation.Transactional; * * @author Jens Schauder * @author Greg Turnquist + * @author Mark Paluch */ @ContextConfiguration @ActiveProfiles("hsql") @@ -86,8 +88,10 @@ public class MyBatisHsqlIntegrationTests { } @Bean - DataAccessStrategy dataAccessStrategy(RelationalMappingContext context, SqlSession sqlSession, EmbeddedDatabase db) { - return MyBatisDataAccessStrategy.createCombinedAccessStrategy(context, new NamedParameterJdbcTemplate(db), + DataAccessStrategy dataAccessStrategy(RelationalMappingContext context, RelationalConverter converter, + SqlSession sqlSession, EmbeddedDatabase db) { + return MyBatisDataAccessStrategy.createCombinedAccessStrategy(context, converter, + new NamedParameterJdbcTemplate(db), sqlSession); } } diff --git a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index 4ef5e98a7..deab95c37 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -36,11 +36,13 @@ import org.junit.Test; import org.mockito.stubbing.Answer; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; 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; @@ -72,14 +74,16 @@ public class SimpleJdbcRepositoryEventsUnitTests { public void before() { RelationalMappingContext context = new RelationalMappingContext(); + RelationalConverter converter = new BasicRelationalConverter(context, new JdbcCustomConversions()); NamedParameterJdbcOperations operations = createIdGeneratingOperations(); SqlGeneratorSource generatorSource = new SqlGeneratorSource(context); this.dataAccessStrategy = spy( - new DefaultDataAccessStrategy(generatorSource, context, operations, new EntityInstantiators())); + new DefaultDataAccessStrategy(generatorSource, context, converter, operations)); - JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, publisher, operations); + JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, publisher, + operations); this.repository = factory.getRepository(DummyEntityRepository.class); } diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java index 065060167..ea8db06ce 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java @@ -25,12 +25,13 @@ import java.text.NumberFormat; import org.junit.Before; import org.junit.Test; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.jdbc.repository.config.ConfigurableRowMapperMap; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.projection.ProjectionFactory; +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.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; @@ -44,10 +45,12 @@ import org.springframework.jdbc.core.namedparam.SqlParameterSource; * * @author Jens Schauder * @author Oliver Gierke + * @author Mark Paluch */ public class JdbcQueryLookupStrategyUnitTests { RelationalMappingContext mappingContext = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS); + RelationalConverter converter = mock(BasicRelationalConverter.class); DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); ProjectionFactory projectionFactory = mock(ProjectionFactory.class); RepositoryMetadata metadata; @@ -79,8 +82,8 @@ public class JdbcQueryLookupStrategyUnitTests { private RepositoryQuery getRepositoryQuery(String name, RowMapperMap rowMapperMap) { - JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(mappingContext, new EntityInstantiators(), - accessStrategy, rowMapperMap, operations); + JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(mappingContext, converter, accessStrategy, + rowMapperMap, operations); return queryLookupStrategy.resolveQuery(getMethod(name), metadata, projectionFactory, namedQueries); } diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java index a034bab4b..1df85c738 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java @@ -29,6 +29,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.repository.RowMapperMap; +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.test.util.ReflectionTestUtils; @@ -40,6 +41,7 @@ import org.springframework.test.util.ReflectionTestUtils; * @author Greg Turnquist * @author Christoph Strobl * @author Oliver Gierke + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class JdbcRepositoryFactoryBeanUnitTests { @@ -65,6 +67,7 @@ public class JdbcRepositoryFactoryBeanUnitTests { factoryBean.setDataAccessStrategy(dataAccessStrategy); factoryBean.setMappingContext(mappingContext); + factoryBean.setConverter(new BasicRelationalConverter(mappingContext)); factoryBean.setApplicationEventPublisher(publisher); factoryBean.afterPropertiesSet(); @@ -89,6 +92,7 @@ public class JdbcRepositoryFactoryBeanUnitTests { public void afterPropertiesSetDefaultsNullablePropertiesCorrectly() { factoryBean.setMappingContext(mappingContext); + factoryBean.setConverter(new BasicRelationalConverter(mappingContext)); factoryBean.setApplicationEventPublisher(publisher); factoryBean.afterPropertiesSet(); diff --git a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 47d08c3ce..1afdfbc23 100644 --- a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -25,14 +25,16 @@ 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.data.convert.EntityInstantiators; +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.JdbcCustomConversions; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; -import org.springframework.data.relational.core.mapping.ConversionCustomizer; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.conversion.BasicRelationalConverter; +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.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; @@ -43,6 +45,7 @@ import org.springframework.transaction.PlatformTransactionManager; * * @author Oliver Gierke * @author Jens Schauder + * @author Mark Paluch */ @Configuration @ComponentScan // To pick up configuration classes (per activated profile) @@ -53,11 +56,9 @@ public class TestConfiguration { @Autowired(required = false) SqlSessionFactory sqlSessionFactory; @Bean - JdbcRepositoryFactory jdbcRepositoryFactory(DataAccessStrategy dataAccessStrategy) { - - RelationalMappingContext context = new RelationalMappingContext(NamingStrategy.INSTANCE); - - return new JdbcRepositoryFactory(dataAccessStrategy, context, publisher, namedParameterJdbcTemplate()); + JdbcRepositoryFactory jdbcRepositoryFactory(DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, + RelationalConverter converter) { + return new JdbcRepositoryFactory(dataAccessStrategy, context, converter, publisher, namedParameterJdbcTemplate()); } @Bean @@ -71,15 +72,28 @@ public class TestConfiguration { } @Bean - DataAccessStrategy defaultDataAccessStrategy(RelationalMappingContext context) { - return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, namedParameterJdbcTemplate(), new EntityInstantiators()); + DataAccessStrategy defaultDataAccessStrategy(RelationalMappingContext context, RelationalConverter converter) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), context, converter, + namedParameterJdbcTemplate()); + } + + @Bean + RelationalMappingContext jdbcMappingContext(NamedParameterJdbcOperations template, + Optional namingStrategy, CustomConversions conversions) { + + RelationalMappingContext mappingContext = new RelationalMappingContext( + namingStrategy.orElse(NamingStrategy.INSTANCE)); + mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + return mappingContext; } @Bean - RelationalMappingContext jdbcMappingContext(NamedParameterJdbcOperations template, Optional namingStrategy, - Optional conversionCustomizer) { + CustomConversions jdbcCustomConversions() { + return new JdbcCustomConversions(); + } - return new RelationalMappingContext(namingStrategy.orElse(NamingStrategy.INSTANCE), - conversionCustomizer.orElse(conversionService -> {})); + @Bean + RelationalConverter relationalConverter(RelationalMappingContext mappingContext, CustomConversions conversions) { + return new BasicRelationalConverter(mappingContext, conversions); } } diff --git a/src/test/java/org/springframework/data/relational/core/conversion/BasicRelationalConverterUnitTests.java b/src/test/java/org/springframework/data/relational/core/conversion/BasicRelationalConverterUnitTests.java new file mode 100644 index 000000000..cf0e8b2c8 --- /dev/null +++ b/src/test/java/org/springframework/data/relational/core/conversion/BasicRelationalConverterUnitTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 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.relational.core.conversion; + +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; +import lombok.Value; + +import org.junit.Test; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.model.ParameterValueProvider; +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.util.ClassTypeInformation; + +/** + * Unit tests for {@link BasicRelationalConverter}. + * + * @author Mark Paluch + */ +public class BasicRelationalConverterUnitTests { + + RelationalMappingContext context = new RelationalMappingContext(); + RelationalConverter converter = new BasicRelationalConverter(context); + + @Test // DATAJDBC-235 + @SuppressWarnings("unchecked") + public void shouldUseConvertingPropertyAccessor() { + + RelationalPersistentEntity entity = (RelationalPersistentEntity) context + .getRequiredPersistentEntity(MyEntity.class); + + MyEntity instance = new MyEntity(); + + PersistentPropertyAccessor accessor = converter.getPropertyAccessor(entity, instance); + RelationalPersistentProperty property = entity.getRequiredPersistentProperty("flag"); + accessor.setProperty(property, "1"); + + assertThat(instance.isFlag()).isTrue(); + } + + @Test // DATAJDBC-235 + public void shouldConvertEnumToString() { + + Object result = converter.writeValue(MyEnum.ON, ClassTypeInformation.from(String.class)); + + assertThat(result).isEqualTo("ON"); + } + + @Test // DATAJDBC-235 + public void shouldConvertStringToEnum() { + + Object result = converter.readValue("OFF", ClassTypeInformation.from(MyEnum.class)); + + assertThat(result).isEqualTo(MyEnum.OFF); + } + + @Test // DATAJDBC-235 + @SuppressWarnings("unchecked") + public void shouldCreateInstance() { + + RelationalPersistentEntity entity = (RelationalPersistentEntity) context + .getRequiredPersistentEntity(MyValue.class); + + MyValue result = converter.createInstance(entity, new ParameterValueProvider() { + @Override + public T getParameterValue(Parameter parameter) { + return (T) "bar"; + } + }); + + assertThat(result.getFoo()).isEqualTo("bar"); + } + + @Data + static class MyEntity { + boolean flag; + } + + @Value + static class MyValue { + final String foo; + } + + enum MyEnum { + ON, OFF; + } +}