Skip to content

Commit a2b7699

Browse files
schaudergregturn
authored andcommitted
DATAJDBC-166 - Allow registration of RowMappers based on result type.
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<Person> will use the type Person in the steps above.
1 parent b49b767 commit a2b7699

File tree

12 files changed

+405
-18
lines changed

12 files changed

+405
-18
lines changed

README.adoc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,34 @@ List<DummyEntity> findByNameRange(@Param("lower") String lower, @Param("upper")
124124

125125
If you compile your sources with the `-parameters` compiler flag you can omit the `@Param` annotations.
126126

127+
==== Custom RowMapper
128+
129+
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.
130+
131+
[source,java]
132+
----
133+
134+
@Bean
135+
RowMapperMap rowMappers() {
136+
return new ConfigurableRowMapperMap() //
137+
.register(Person.class, new PersonRowMapper()) //
138+
.register(Address.class, new AddressRowMapper());
139+
}
140+
141+
----
142+
143+
When determining the `RowMapper` to use for a method the following steps are followed based on the return type of the method:
144+
145+
1. If the type is a simple type no `RowMapper` is used.
146+
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.
147+
148+
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.
149+
The `RowMapper` registered for that class is used.
150+
Iterating happens in the order of registration, so make sure to register more general types after specific ones.
151+
152+
If applicable wrapper type like collections or `Optional` are unwrapped.
153+
So a return type of `Optional<Person>` will use the type `Person` in the steps above.
154+
127155
=== Id generation
128156

129157
Spring Data JDBC uses the id to identify entities but also to determine if an entity is new or already existing in the database.

src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ private <S> Optional<Object> getIdFromHolder(KeyHolder holder, JdbcPersistentEnt
283283
}
284284

285285
public <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
286-
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context.getConversions(), context, accessStrategy);
286+
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context, accessStrategy);
287287
}
288288

289289
private RowMapper getMapEntityRowMapper(JdbcPersistentProperty property) {

src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ public class EntityRowMapper<T> implements RowMapper<T> {
5353
private final DataAccessStrategy accessStrategy;
5454
private final JdbcPersistentProperty idProperty;
5555

56-
public EntityRowMapper(JdbcPersistentEntity<T> entity, ConversionService conversions, JdbcMappingContext context,
57-
DataAccessStrategy accessStrategy) {
56+
public EntityRowMapper(JdbcPersistentEntity<T> entity, JdbcMappingContext context,
57+
DataAccessStrategy accessStrategy) {
5858

5959
this.entity = entity;
60-
this.conversions = conversions;
60+
this.conversions = context.getConversions();
6161
this.context = context;
6262
this.accessStrategy = accessStrategy;
6363

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.repository;
17+
18+
import org.springframework.jdbc.core.RowMapper;
19+
20+
/**
21+
* A map from a type to a {@link RowMapper} to be used for extracting that type from {@link java.sql.ResultSet}s.
22+
*
23+
* @author Jens Schauder
24+
*/
25+
public interface RowMapperMap {
26+
27+
/**
28+
* An immutable empty instance that will return {@literal null} for all arguments.
29+
*/
30+
RowMapperMap EMPTY = new RowMapperMap() {
31+
32+
public <T> RowMapper<? extends T> rowMapperFor(Class<T> type) {
33+
return null;
34+
}
35+
};
36+
37+
<T> RowMapper<? extends T> rowMapperFor(Class<T> type);
38+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.repository.config;
17+
18+
import java.util.LinkedHashMap;
19+
import java.util.Map;
20+
21+
import org.springframework.data.jdbc.repository.RowMapperMap;
22+
import org.springframework.jdbc.core.RowMapper;
23+
24+
/**
25+
* A {@link RowMapperMap} that allows for registration of {@link RowMapper}s via a fluent Api.
26+
*
27+
* @author Jens Schauder
28+
*/
29+
public class ConfigurableRowMapperMap implements RowMapperMap {
30+
31+
private Map<Class<?>, RowMapper<?>> rowMappers = new LinkedHashMap<>();
32+
33+
/**
34+
* Registers a the given {@link RowMapper} as to be used for the given type.
35+
*
36+
* @return this instance, so this can be used as a fluent interface.
37+
*/
38+
public <T> ConfigurableRowMapperMap register(Class<T> type, RowMapper<? extends T> rowMapper) {
39+
40+
rowMappers.put(type, rowMapper);
41+
return this;
42+
}
43+
44+
@SuppressWarnings("unchecked")
45+
public <T> RowMapper<? extends T> rowMapperFor(Class<T> type) {
46+
47+
RowMapper<? extends T> candidate = (RowMapper<? extends T>) rowMappers.get(type);
48+
49+
if (candidate == null) {
50+
51+
for (Map.Entry<Class<?>, RowMapper<?>> entry : rowMappers.entrySet()) {
52+
53+
if (type.isAssignableFrom(entry.getKey())) {
54+
candidate = (RowMapper<? extends T>) entry.getValue();
55+
}
56+
}
57+
}
58+
59+
return candidate;
60+
}
61+
}

src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.data.jdbc.core.DataAccessStrategy;
2222
import org.springframework.data.jdbc.core.EntityRowMapper;
2323
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
24+
import org.springframework.data.jdbc.repository.RowMapperMap;
2425
import org.springframework.data.projection.ProjectionFactory;
2526
import org.springframework.data.repository.core.NamedQueries;
2627
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -40,13 +41,15 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {
4041

4142
private final JdbcMappingContext context;
4243
private final DataAccessStrategy accessStrategy;
44+
private final RowMapperMap rowMapperMap;
4345
private final ConversionService conversionService;
4446

4547
JdbcQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider, JdbcMappingContext context,
46-
DataAccessStrategy accessStrategy) {
48+
DataAccessStrategy accessStrategy, RowMapperMap rowMapperMap) {
4749

4850
this.context = context;
4951
this.accessStrategy = accessStrategy;
52+
this.rowMapperMap = rowMapperMap;
5053
this.conversionService = context.getConversions();
5154
}
5255

@@ -55,22 +58,32 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository
5558
ProjectionFactory projectionFactory, NamedQueries namedQueries) {
5659

5760
JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory);
58-
Class<?> returnedObjectType = queryMethod.getReturnedObjectType();
5961

60-
RowMapper<?> rowMapper = queryMethod.isModifyingQuery() ? null : createRowMapper(returnedObjectType);
62+
RowMapper<?> rowMapper = queryMethod.isModifyingQuery() ? null : createRowMapper(queryMethod);
6163

6264
return new JdbcRepositoryQuery(queryMethod, context, rowMapper);
6365
}
6466

65-
private RowMapper<?> createRowMapper(Class<?> returnedObjectType) {
67+
private RowMapper<?> createRowMapper(JdbcQueryMethod queryMethod) {
68+
69+
Class<?> returnedObjectType = queryMethod.getReturnedObjectType();
6670

6771
return context.getSimpleTypeHolder().isSimpleType(returnedObjectType)
6872
? SingleColumnRowMapper.newInstance(returnedObjectType, conversionService)
69-
: new EntityRowMapper<>( //
70-
context.getRequiredPersistentEntity(returnedObjectType), //
71-
conversionService, //
73+
: determineDefaultRowMapper(queryMethod);
74+
}
75+
76+
private RowMapper<?> determineDefaultRowMapper(JdbcQueryMethod queryMethod) {
77+
78+
Class<?> domainType = queryMethod.getReturnedObjectType();
79+
80+
RowMapper typeMappedRowMapper = rowMapperMap.rowMapperFor(domainType);
81+
82+
return typeMappedRowMapper == null //
83+
? new EntityRowMapper<>( //
84+
context.getRequiredPersistentEntity(domainType), //
7285
context, //
73-
accessStrategy //
74-
);
86+
accessStrategy) //
87+
: typeMappedRowMapper;
7588
}
7689
}

src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.data.jdbc.core.JdbcEntityTemplate;
2323
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
2424
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
25+
import org.springframework.data.jdbc.repository.RowMapperMap;
2526
import org.springframework.data.jdbc.repository.SimpleJdbcRepository;
2627
import org.springframework.data.repository.core.EntityInformation;
2728
import org.springframework.data.repository.core.RepositoryInformation;
@@ -42,6 +43,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
4243
private final JdbcMappingContext context;
4344
private final ApplicationEventPublisher publisher;
4445
private final DataAccessStrategy accessStrategy;
46+
private RowMapperMap rowMapperMap = RowMapperMap.EMPTY;
4547

4648
public JdbcRepositoryFactory(ApplicationEventPublisher publisher, JdbcMappingContext context,
4749
DataAccessStrategy dataAccessStrategy) {
@@ -84,6 +86,10 @@ protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrate
8486
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
8587
}
8688

87-
return Optional.of(new JdbcQueryLookupStrategy(evaluationContextProvider, context, accessStrategy));
89+
return Optional.of(new JdbcQueryLookupStrategy(evaluationContextProvider, context, accessStrategy, rowMapperMap));
90+
}
91+
92+
public void setRowMapperMap(RowMapperMap rowMapperMap) {
93+
this.rowMapperMap = rowMapperMap;
8894
}
8995
}

src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.context.ApplicationEventPublisherAware;
2323
import org.springframework.data.jdbc.core.DataAccessStrategy;
2424
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
25+
import org.springframework.data.jdbc.repository.RowMapperMap;
2526
import org.springframework.data.repository.Repository;
2627
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
2728
import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport;
@@ -41,6 +42,7 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extend
4142
private ApplicationEventPublisher publisher;
4243
private JdbcMappingContext mappingContext;
4344
private DataAccessStrategy dataAccessStrategy;
45+
private RowMapperMap rowMapperMap = RowMapperMap.EMPTY;
4446

4547
JdbcRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
4648
super(repositoryInterface);
@@ -60,7 +62,15 @@ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
6062
*/
6163
@Override
6264
protected RepositoryFactorySupport doCreateRepositoryFactory() {
63-
return new JdbcRepositoryFactory(publisher, mappingContext, dataAccessStrategy);
65+
66+
JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(publisher, mappingContext,
67+
dataAccessStrategy);
68+
69+
if (rowMapperMap != null) {
70+
jdbcRepositoryFactory.setRowMapperMap(rowMapperMap);
71+
}
72+
73+
return jdbcRepositoryFactory;
6474
}
6575

6676
@Autowired
@@ -75,6 +85,11 @@ public void setDataAccessStrategy(DataAccessStrategy dataAccessStrategy) {
7585
this.dataAccessStrategy = dataAccessStrategy;
7686
}
7787

88+
@Autowired(required = false)
89+
public void setRowMapperMap(RowMapperMap rowMapperMap) {
90+
this.rowMapperMap = rowMapperMap;
91+
}
92+
7893
@Override
7994
public void afterPropertiesSet() {
8095

src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
2929
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
3030
import org.springframework.data.jdbc.mapping.model.NamingStrategy;
31-
import org.springframework.data.repository.query.Param;
3231
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
3332
import org.springframework.util.Assert;
3433

@@ -204,7 +203,7 @@ private <T> EntityRowMapper<T> createRowMapper(Class<T> type, NamingStrategy nam
204203
Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter);
205204

206205
return new EntityRowMapper<>((JdbcPersistentEntity<T>) context.getRequiredPersistentEntity(type),
207-
conversionService, context, accessStrategy);
206+
context, accessStrategy);
208207
}
209208

210209
private static ResultSet mockResultSet(List<String> columns, Object... values) {

0 commit comments

Comments
 (0)