From 9908ec5acc7ba08b0c84834e5d5e8f0f6edb035a Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 19 Mar 2018 10:09:55 +0100 Subject: [PATCH 1/3] DATAJDBC-166 Prepare branch --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 9db1d4ca8d9a3a61aeedc694fe0453d0079c2d3d Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 17 Jan 2018 13:13:29 +0100 Subject: [PATCH 2/3] DATAJDBC-166 - Allow registration of RowMappers based on result type. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RowMapper can be configured either via the @Query(rowMapperClass = …​.) or by registerign a RowMapperMap bean and register RowMapper per method return type. @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. --- README.adoc | 28 ++++++ .../jdbc/core/DefaultDataAccessStrategy.java | 2 +- .../data/jdbc/core/EntityRowMapper.java | 6 +- .../data/jdbc/repository/RowMapperMap.java | 38 ++++++++ .../config/ConfigurableRowMapperMap.java | 61 ++++++++++++ .../support/JdbcQueryLookupStrategy.java | 31 ++++-- .../support/JdbcRepositoryFactory.java | 8 +- .../support/JdbcRepositoryFactoryBean.java | 17 +++- .../jdbc/core/EntityRowMapperUnitTests.java | 3 +- .../ConfigurableRowMapperMapUnitTests.java | 95 ++++++++++++++++++ ...nableJdbcRepositoriesIntegrationTests.java | 37 ++++++- .../JdbcQueryLookupStrategyUnitTests.java | 97 +++++++++++++++++++ 12 files changed, 405 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/repository/RowMapperMap.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/config/ConfigurableRowMapperMap.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/config/ConfigurableRowMapperMapUnitTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java 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/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index bbade9dc5c..af72d1331b 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -283,7 +283,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..808283c83b 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -53,11 +53,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; 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..6cb6d981c6 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,8 +48,18 @@ @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() { @@ -52,6 +70,15 @@ public void repositoryGetsPickedUp() { assertNotNull(all); } + @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; + } + +} From 49b99c2b75fbae8a14463ea9c5e195300f0efc55 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 19 Mar 2018 12:25:07 +0100 Subject: [PATCH 3/3] DATAJDBC-166 - Polishing. Formatting. Import order. Use of AssertJ. --- .../jdbc/core/DefaultDataAccessStrategy.java | 19 ++++++++-------- .../data/jdbc/core/EntityRowMapper.java | 22 +++++++++---------- ...nableJdbcRepositoriesIntegrationTests.java | 4 ++-- 3 files changed, 22 insertions(+), 23 deletions(-) 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 af72d1331b..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); 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 808283c83b..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. * @@ -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/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index 6cb6d981c6..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 @@ -63,11 +63,11 @@ public static void setup() { @Test // DATAJDBC-100 public void repositoryGetsPickedUp() { - assertNotNull(repository); + assertThat(repository).isNotNull(); Iterable all = repository.findAll(); - assertNotNull(all); + assertThat(all).isNotNull(); } @Test // DATAJDBC-166