diff --git a/README.adoc b/README.adoc index 42ae7cf71f..9882edaef7 100644 --- a/README.adoc +++ b/README.adoc @@ -124,6 +124,34 @@ List findByNameRange(@Param("lower") String lower, @Param("upper") If you compile your sources with the `-parameters` compiler flag you can omit the `@Param` annotations. +==== Custom RowMapper + +You can configure the `RowMapper` to use using either the `@Query(rowMapperClass = ....)` or you can register a `RowMapperMap` bean and register `RowMapper` per method return type. + +[source,java] +---- + + @Bean + RowMapperMap rowMappers() { + return new ConfigurableRowMapperMap() // + .register(Person.class, new PersonRowMapper()) // + .register(Address.class, new AddressRowMapper()); + } + +---- + +When determining the `RowMapper` to use for a method the following steps are followed based on the return type of the method: + +1. If the type is a simple type no `RowMapper` is used. + Instead the query is expected to return a single row with a single column and a conversion to the return type is applied to that value. + +2. The entity classes in the `RowMapperMap` are iterated until one is found that is a superclass or interface of the return type in question. + The `RowMapper` registered for that class is used. + Iterating happens in the order of registration, so make sure to register more general types after specific ones. + +If applicable wrapper type like collections or `Optional` are unwrapped. +So a return type of `Optional` will use the type `Person` in the steps above. + === Id generation Spring Data JDBC uses the id to identify entities but also to determine if an entity is new or already existing in the database. diff --git a/pom.xml b/pom.xml index 634c3fe1f5..39993f17d5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jdbc - 1.0.0.BUILD-SNAPSHOT + 1.0.0.DATAJDBC-166-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. 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 bbade9dc5c..3297d9d4a9 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -15,6 +15,12 @@ */ package org.springframework.data.jdbc.core; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.NonTransientDataAccessException; @@ -34,12 +40,6 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.util.Assert; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - /** * The default {@link DataAccessStrategy} is to generate SQL statements based on meta data from the entity. * @@ -57,7 +57,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { private final DataAccessStrategy accessStrategy; public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedParameterJdbcOperations operations, - JdbcMappingContext context, DataAccessStrategy accessStrategy) { + JdbcMappingContext context, DataAccessStrategy accessStrategy) { this.sqlGeneratorSource = sqlGeneratorSource; this.operations = operations; @@ -70,7 +70,7 @@ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedPar * Only suitable if this is the only access strategy in use. */ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedParameterJdbcOperations operations, - JdbcMappingContext context) { + JdbcMappingContext context) { this.sqlGeneratorSource = sqlGeneratorSource; this.operations = operations; @@ -236,7 +236,8 @@ private MapSqlParameterSource getPropertyMap(final S instance, JdbcPersisten @SuppressWarnings("unchecked") private ID getIdValueOrNull(S instance, JdbcPersistentEntity persistentEntity) { - EntityInformation entityInformation = (EntityInformation) context.getRequiredPersistentEntityInformation(persistentEntity.getType()); + EntityInformation entityInformation = (EntityInformation) context + .getRequiredPersistentEntityInformation(persistentEntity.getType()); ID idValue = entityInformation.getId(instance); @@ -283,7 +284,7 @@ private Optional getIdFromHolder(KeyHolder holder, JdbcPersistentEnt } public EntityRowMapper getEntityRowMapper(Class domainType) { - return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context.getConversions(), context, accessStrategy); + return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context, accessStrategy); } private RowMapper getMapEntityRowMapper(JdbcPersistentProperty property) { 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 73b28b155d..78b80852ac 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -17,6 +17,10 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; + +import java.sql.ResultSet; +import java.sql.SQLException; + import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ClassGeneratingEntityInstantiator; @@ -32,9 +36,6 @@ import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.jdbc.core.RowMapper; -import java.sql.ResultSet; -import java.sql.SQLException; - /** * Maps a ResultSet to an entity of type {@code T}, including entities referenced. * @@ -53,11 +54,11 @@ public class EntityRowMapper implements RowMapper { private final DataAccessStrategy accessStrategy; private final JdbcPersistentProperty idProperty; - public EntityRowMapper(JdbcPersistentEntity entity, ConversionService conversions, JdbcMappingContext context, - DataAccessStrategy accessStrategy) { + public EntityRowMapper(JdbcPersistentEntity entity, JdbcMappingContext context, + DataAccessStrategy accessStrategy) { this.entity = entity; - this.conversions = conversions; + this.conversions = context.getConversions(); this.context = context; this.accessStrategy = accessStrategy; @@ -124,7 +125,8 @@ private S readEntityFrom(ResultSet rs, PersistentProperty property) { return null; } - S instance = instantiator.createInstance(entity, new ResultSetParameterValueProvider(rs, entity, conversions, prefix)); + S instance = instantiator.createInstance(entity, + new ResultSetParameterValueProvider(rs, entity, conversions, prefix)); PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); @@ -139,14 +141,10 @@ private S readEntityFrom(ResultSet rs, PersistentProperty property) { @RequiredArgsConstructor private static class ResultSetParameterValueProvider implements ParameterValueProvider { - @NonNull - private final ResultSet resultSet; - @NonNull - private final JdbcPersistentEntity entity; - @NonNull - private final ConversionService conversionService; - @NonNull - private final String prefix; + @NonNull private final ResultSet resultSet; + @NonNull private final JdbcPersistentEntity entity; + @NonNull private final ConversionService conversionService; + @NonNull private final String prefix; /* * (non-Javadoc) diff --git a/src/main/java/org/springframework/data/jdbc/repository/RowMapperMap.java b/src/main/java/org/springframework/data/jdbc/repository/RowMapperMap.java new file mode 100644 index 0000000000..cbf0110b68 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/RowMapperMap.java @@ -0,0 +1,38 @@ +/* + * 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.repository; + +import org.springframework.jdbc.core.RowMapper; + +/** + * A map from a type to a {@link RowMapper} to be used for extracting that type from {@link java.sql.ResultSet}s. + * + * @author Jens Schauder + */ +public interface RowMapperMap { + + /** + * An immutable empty instance that will return {@literal null} for all arguments. + */ + RowMapperMap EMPTY = new RowMapperMap() { + + public RowMapper rowMapperFor(Class type) { + return null; + } + }; + + RowMapper rowMapperFor(Class type); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/ConfigurableRowMapperMap.java b/src/main/java/org/springframework/data/jdbc/repository/config/ConfigurableRowMapperMap.java new file mode 100644 index 0000000000..a3544a82c1 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/config/ConfigurableRowMapperMap.java @@ -0,0 +1,61 @@ +/* + * 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.repository.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.jdbc.repository.RowMapperMap; +import org.springframework.jdbc.core.RowMapper; + +/** + * A {@link RowMapperMap} that allows for registration of {@link RowMapper}s via a fluent Api. + * + * @author Jens Schauder + */ +public class ConfigurableRowMapperMap implements RowMapperMap { + + private Map, RowMapper> rowMappers = new LinkedHashMap<>(); + + /** + * Registers a the given {@link RowMapper} as to be used for the given type. + * + * @return this instance, so this can be used as a fluent interface. + */ + public ConfigurableRowMapperMap register(Class type, RowMapper rowMapper) { + + rowMappers.put(type, rowMapper); + return this; + } + + @SuppressWarnings("unchecked") + public RowMapper rowMapperFor(Class type) { + + RowMapper candidate = (RowMapper) rowMappers.get(type); + + if (candidate == null) { + + for (Map.Entry, RowMapper> entry : rowMappers.entrySet()) { + + if (type.isAssignableFrom(entry.getKey())) { + candidate = (RowMapper) entry.getValue(); + } + } + } + + return candidate; + } +} 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 ccc6039faa..8df58d89be 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 @@ -21,6 +21,7 @@ import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.EntityRowMapper; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; @@ -40,13 +41,15 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { private final JdbcMappingContext context; private final DataAccessStrategy accessStrategy; + private final RowMapperMap rowMapperMap; private final ConversionService conversionService; JdbcQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider, JdbcMappingContext context, - DataAccessStrategy accessStrategy) { + DataAccessStrategy accessStrategy, RowMapperMap rowMapperMap) { this.context = context; this.accessStrategy = accessStrategy; + this.rowMapperMap = rowMapperMap; this.conversionService = context.getConversions(); } @@ -55,22 +58,32 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository ProjectionFactory projectionFactory, NamedQueries namedQueries) { JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory); - Class returnedObjectType = queryMethod.getReturnedObjectType(); - RowMapper rowMapper = queryMethod.isModifyingQuery() ? null : createRowMapper(returnedObjectType); + RowMapper rowMapper = queryMethod.isModifyingQuery() ? null : createRowMapper(queryMethod); return new JdbcRepositoryQuery(queryMethod, context, rowMapper); } - private RowMapper createRowMapper(Class returnedObjectType) { + private RowMapper createRowMapper(JdbcQueryMethod queryMethod) { + + Class returnedObjectType = queryMethod.getReturnedObjectType(); return context.getSimpleTypeHolder().isSimpleType(returnedObjectType) ? SingleColumnRowMapper.newInstance(returnedObjectType, conversionService) - : new EntityRowMapper<>( // - context.getRequiredPersistentEntity(returnedObjectType), // - conversionService, // + : determineDefaultRowMapper(queryMethod); + } + + private RowMapper determineDefaultRowMapper(JdbcQueryMethod queryMethod) { + + Class domainType = queryMethod.getReturnedObjectType(); + + RowMapper typeMappedRowMapper = rowMapperMap.rowMapperFor(domainType); + + return typeMappedRowMapper == null // + ? new EntityRowMapper<>( // + context.getRequiredPersistentEntity(domainType), // context, // - accessStrategy // - ); + 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 49281dc738..288bc452cd 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 @@ -22,6 +22,7 @@ import org.springframework.data.jdbc.core.JdbcEntityTemplate; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation; +import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.jdbc.repository.SimpleJdbcRepository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; @@ -42,6 +43,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { private final JdbcMappingContext context; private final ApplicationEventPublisher publisher; private final DataAccessStrategy accessStrategy; + private RowMapperMap rowMapperMap = RowMapperMap.EMPTY; public JdbcRepositoryFactory(ApplicationEventPublisher publisher, JdbcMappingContext context, DataAccessStrategy dataAccessStrategy) { @@ -84,6 +86,10 @@ protected Optional getQueryLookupStrategy(QueryLookupStrate throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); } - return Optional.of(new JdbcQueryLookupStrategy(evaluationContextProvider, context, accessStrategy)); + return Optional.of(new JdbcQueryLookupStrategy(evaluationContextProvider, context, accessStrategy, rowMapperMap)); + } + + public void setRowMapperMap(RowMapperMap rowMapperMap) { + this.rowMapperMap = rowMapperMap; } } 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 533122528a..4f53754dc3 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 @@ -22,6 +22,7 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; @@ -41,6 +42,7 @@ public class JdbcRepositoryFactoryBean, S, ID extend private ApplicationEventPublisher publisher; private JdbcMappingContext mappingContext; private DataAccessStrategy dataAccessStrategy; + private RowMapperMap rowMapperMap = RowMapperMap.EMPTY; JdbcRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); @@ -60,7 +62,15 @@ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { */ @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { - return new JdbcRepositoryFactory(publisher, mappingContext, dataAccessStrategy); + + JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(publisher, mappingContext, + dataAccessStrategy); + + if (rowMapperMap != null) { + jdbcRepositoryFactory.setRowMapperMap(rowMapperMap); + } + + return jdbcRepositoryFactory; } @Autowired @@ -75,6 +85,11 @@ public void setDataAccessStrategy(DataAccessStrategy dataAccessStrategy) { this.dataAccessStrategy = dataAccessStrategy; } + @Autowired(required = false) + public void setRowMapperMap(RowMapperMap rowMapperMap) { + this.rowMapperMap = rowMapperMap; + } + @Override public void afterPropertiesSet() { 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 36798f02e5..b9471ce460 100644 --- a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java @@ -28,7 +28,6 @@ import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; import org.springframework.data.jdbc.mapping.model.NamingStrategy; -import org.springframework.data.repository.query.Param; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.util.Assert; @@ -204,7 +203,7 @@ private EntityRowMapper createRowMapper(Class type, NamingStrategy nam Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter); return new EntityRowMapper<>((JdbcPersistentEntity) context.getRequiredPersistentEntity(type), - conversionService, context, accessStrategy); + context, accessStrategy); } private static ResultSet mockResultSet(List columns, Object... values) { diff --git a/src/test/java/org/springframework/data/jdbc/repository/config/ConfigurableRowMapperMapUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/ConfigurableRowMapperMapUnitTests.java new file mode 100644 index 0000000000..6861d9a4a4 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/config/ConfigurableRowMapperMapUnitTests.java @@ -0,0 +1,95 @@ +/* + * 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.repository.config; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.springframework.data.jdbc.repository.RowMapperMap; +import org.springframework.jdbc.core.RowMapper; + +/** + * Unit tests for {@link ConfigurableRowMapperMap}. + * + * @author Jens Schauder + */ +public class ConfigurableRowMapperMapUnitTests { + + @Test + public void freshInstanceReturnsNull() { + + RowMapperMap map = new ConfigurableRowMapperMap(); + + assertThat(map.rowMapperFor(Object.class)).isNull(); + } + + @Test + public void returnsConfiguredInstanceForClass() { + + RowMapper rowMapper = mock(RowMapper.class); + + RowMapperMap map = new ConfigurableRowMapperMap().register(Object.class, rowMapper); + + assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper); + } + + @Test + public void returnsNullForClassNotConfigured() { + + RowMapper rowMapper = mock(RowMapper.class); + + RowMapperMap map = new ConfigurableRowMapperMap().register(Number.class, rowMapper); + + assertThat(map.rowMapperFor(Integer.class)).isNull(); + assertThat(map.rowMapperFor(String.class)).isNull(); + } + + @Test + public void returnsInstanceRegisteredForSubClass() { + + RowMapper rowMapper = mock(RowMapper.class); + + RowMapperMap map = new ConfigurableRowMapperMap().register(String.class, rowMapper); + + assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper); + } + + @Test + public void prefersExactTypeMatchClass() { + + RowMapper rowMapper = mock(RowMapper.class); + + RowMapperMap map = new ConfigurableRowMapperMap() // + .register(Object.class, mock(RowMapper.class)) // + .register(Integer.class, rowMapper) // + .register(Number.class, mock(RowMapper.class)); + + assertThat(map.rowMapperFor(Integer.class)).isEqualTo(rowMapper); + } + + @Test + public void prefersLatestRegistrationForSuperTypeMatch() { + + RowMapper rowMapper = mock(RowMapper.class); + + RowMapperMap map = new ConfigurableRowMapperMap() // + .register(Integer.class, mock(RowMapper.class)) // + .register(Number.class, rowMapper); + + assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index 91529b8da4..9b0edde443 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -15,20 +15,28 @@ */ package org.springframework.data.jdbc.repository.config; -import static org.junit.Assert.assertNotNull; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import lombok.Data; +import java.lang.reflect.Field; + +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.RowMapper; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.ReflectionUtils; /** * Tests the {@link EnableJdbcRepositories} annotation. @@ -40,16 +48,35 @@ @ContextConfiguration(classes = TestConfiguration.class) public class EnableJdbcRepositoriesIntegrationTests { + static final Field ROW_MAPPER_MAP = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "rowMapperMap"); + public static final RowMapper DUMMY_ENTITY_ROW_MAPPER = mock(RowMapper.class); + public static final RowMapper STRING_ROW_MAPPER = mock(RowMapper.class); + + @Autowired JdbcRepositoryFactoryBean factoryBean; @Autowired DummyRepository repository; + @BeforeClass + public static void setup() { + ROW_MAPPER_MAP.setAccessible(true); + } + @Test // DATAJDBC-100 public void repositoryGetsPickedUp() { - assertNotNull(repository); + assertThat(repository).isNotNull(); Iterable all = repository.findAll(); - assertNotNull(all); + assertThat(all).isNotNull(); + } + + @Test // DATAJDBC-166 + public void customRowMapperConfigurationGetsPickedUp() { + + RowMapperMap mapping = (RowMapperMap) ReflectionUtils.getField(ROW_MAPPER_MAP, factoryBean); + + assertThat(mapping.rowMapperFor(String.class)).isEqualTo(STRING_ROW_MAPPER); + assertThat(mapping.rowMapperFor(DummyEntity.class)).isEqualTo(DUMMY_ENTITY_ROW_MAPPER); } interface DummyRepository extends CrudRepository { @@ -69,5 +96,13 @@ static class TestConfiguration { Class testClass() { return EnableJdbcRepositoriesIntegrationTests.class; } + + @Bean + RowMapperMap rowMappers() { + return new ConfigurableRowMapperMap() // + .register(DummyEntity.class, DUMMY_ENTITY_ROW_MAPPER) // + .register(String.class, STRING_ROW_MAPPER); + } + } } 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 new file mode 100644 index 0000000000..68a953bed7 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java @@ -0,0 +1,97 @@ +/* + * 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.repository.support; + +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.text.NumberFormat; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.jdbc.core.DataAccessStrategy; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +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.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * Unit tests for {@link JdbcQueryLookupStrategy}. + * + * @author Jens Schauder + */ +public class JdbcQueryLookupStrategyUnitTests { + + EvaluationContextProvider evaluationContextProvider = mock(EvaluationContextProvider.class); + JdbcMappingContext mappingContext = mock(JdbcMappingContext.class, RETURNS_DEEP_STUBS); + DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); + ProjectionFactory projectionFactory = mock(ProjectionFactory.class); + RepositoryMetadata metadata; + NamedQueries namedQueries = mock(NamedQueries.class); + + @Before + public void setup() { + + metadata = mock(RepositoryMetadata.class); + when(metadata.getReturnedDomainClass(any(Method.class))).thenReturn((Class) NumberFormat.class); + + } + + private Method getMethod(String name) { + + try { + return this.getClass().getDeclaredMethod(name); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + @Test // DATAJDBC-166 + public void typeBasedRowMapperGetsUsedForQuery() { + + RowMapper numberFormatMapper = mock(RowMapper.class); + RowMapperMap rowMapperMap = new ConfigurableRowMapperMap().register(NumberFormat.class, numberFormatMapper); + + RepositoryQuery repositoryQuery = getRepositoryQuery("returningNumberFormat", rowMapperMap); + + repositoryQuery.execute(new Object[] {}); + + verify(mappingContext.getTemplate()).queryForObject(anyString(), any(SqlParameterSource.class), + eq(numberFormatMapper)); + } + + private RepositoryQuery getRepositoryQuery(String name, RowMapperMap rowMapperMap) { + + JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(evaluationContextProvider, mappingContext, + accessStrategy, rowMapperMap); + + return queryLookupStrategy.resolveQuery(getMethod(name), metadata, projectionFactory, namedQueries); + } + + // NumberFormat is just used as an arbitrary non simple type. + @Query("some SQL") + private NumberFormat returningNumberFormat() { + return null; + } + +}