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.
This commit is contained in:
Mark Paluch
2018-07-17 15:40:51 +02:00
committed by Jens Schauder
parent 9ad0cf0296
commit fb858bf1b1
22 changed files with 835 additions and 241 deletions

View File

@@ -20,14 +20,21 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.HashMap;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
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.RelationalMappingContext;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
@@ -37,6 +44,7 @@ import org.springframework.jdbc.support.KeyHolder;
* Unit tests for {@link DefaultDataAccessStrategy}.
*
* @author Jens Schauder
* @author Mark Paluch
*/
public class DefaultDataAccessStrategyUnitTests {
@@ -45,15 +53,15 @@ public class DefaultDataAccessStrategyUnitTests {
NamedParameterJdbcOperations jdbcOperations = mock(NamedParameterJdbcOperations.class);
RelationalMappingContext context = new RelationalMappingContext();
RelationalConverter converter = new BasicRelationalConverter(context, new JdbcCustomConversions());
HashMap<String, Object> additionalParameters = new HashMap<>();
ArgumentCaptor<SqlParameterSource> 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<String> 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<Boolean, String> {
INSTANCE;
@Override
public String convert(Boolean source) {
return source != null && source ? "T" : "F";
}
}
@ReadingConverter
enum StringToBooleanConverter implements Converter<String, Boolean> {
INSTANCE;
@Override
public Boolean convert(String source) {
return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE;
}
}
}

View File

@@ -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<T>) context.getRequiredPersistentEntity(type), //
context, //
new EntityInstantiators(), //
converter, //
accessStrategy //
);
}

View File

@@ -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() {

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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> namingStrategy,
Optional<ConversionCustomizer> conversionCustomizer) {
RelationalMappingContext jdbcMappingContext(NamedParameterJdbcOperations template,
Optional<NamingStrategy> namingStrategy, CustomConversions conversions) {
return new RelationalMappingContext(namingStrategy.orElse(NamingStrategy.INSTANCE),
conversionCustomizer.orElse(conversionService -> {}));
RelationalMappingContext mappingContext = new RelationalMappingContext(
namingStrategy.orElse(NamingStrategy.INSTANCE));
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
return mappingContext;
}
@Bean
CustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions();
}
@Bean
RelationalConverter relationalConverter(RelationalMappingContext mappingContext, CustomConversions conversions) {
return new BasicRelationalConverter(mappingContext, conversions);
}
}

View File

@@ -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<MyEntity> entity = (RelationalPersistentEntity) context
.getRequiredPersistentEntity(MyEntity.class);
MyEntity instance = new MyEntity();
PersistentPropertyAccessor<MyEntity> 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<MyValue> entity = (RelationalPersistentEntity) context
.getRequiredPersistentEntity(MyValue.class);
MyValue result = converter.createInstance(entity, new ParameterValueProvider<RelationalPersistentProperty>() {
@Override
public <T> T getParameterValue(Parameter<T, RelationalPersistentProperty> 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;
}
}