Skip to content

Commit 7687e3e

Browse files
committed
DATAJDBC-166 - Allow registration of RowMappers based on result type.
1 parent 362f38e commit 7687e3e

File tree

10 files changed

+387
-9
lines changed

10 files changed

+387
-9
lines changed

README.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,23 @@ List<DummyEntity> findByNameRange(@Param("lower") String lower, @Param("upper")
7070

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

73+
==== Custom RowMapper
74+
75+
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.
76+
77+
[source,java]
78+
----
79+
80+
@Bean
81+
RowMapperMap rowMappers() {
82+
return new ConfigurableRowMapperMap() //
83+
.register(DummyEntity.class, new MyDummyEntityRowMapper()) //
84+
.register(String.class, new StringRowMapper());
85+
}
86+
87+
----
88+
89+
7390
=== Id generation
7491

7592
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/EntityRowMapper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class EntityRowMapper<T> implements RowMapper<T> {
5252
private final DataAccessStrategy accessStrategy;
5353
private final JdbcPersistentProperty idProperty;
5454

55+
// TODO: should just take a context and obtain conversions from there.
5556
public EntityRowMapper(JdbcPersistentEntity<T> entity, ConversionService conversions, JdbcMappingContext context,
5657
DataAccessStrategy accessStrategy) {
5758

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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 of {@link RowMapper}s.
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+
for (Map.Entry<Class<?>, RowMapper<?>> entry : rowMappers.entrySet()) {
51+
if (type.isAssignableFrom(entry.getKey())) {
52+
candidate = (RowMapper<? extends T>) entry.getValue();
53+
}
54+
}
55+
}
56+
57+
return candidate;
58+
}
59+
}

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.springframework.data.jdbc.core.DataAccessStrategy;
2121
import org.springframework.data.jdbc.core.EntityRowMapper;
2222
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
23+
import org.springframework.data.jdbc.repository.RowMapperMap;
2324
import org.springframework.data.projection.ProjectionFactory;
2425
import org.springframework.data.repository.core.NamedQueries;
2526
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -37,23 +38,38 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {
3738

3839
private final JdbcMappingContext context;
3940
private final DataAccessStrategy accessStrategy;
41+
private final RowMapperMap rowMapperMap;
4042

4143
JdbcQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider, JdbcMappingContext context,
42-
DataAccessStrategy accessStrategy) {
44+
DataAccessStrategy accessStrategy, RowMapperMap rowMapperMap) {
4345

4446
this.context = context;
4547
this.accessStrategy = accessStrategy;
48+
this.rowMapperMap = rowMapperMap;
4649
}
4750

4851
@Override
4952
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata,
5053
ProjectionFactory projectionFactory, NamedQueries namedQueries) {
5154

5255
JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory);
53-
Class<?> domainType = queryMethod.getReturnedObjectType();
54-
RowMapper<?> rowMapper = new EntityRowMapper<>(context.getRequiredPersistentEntity(domainType),
55-
context.getConversions(), context, accessStrategy);
56+
RowMapper<?> rowMapper = determineDefaultRowMapper(queryMethod);
5657

5758
return new JdbcRepositoryQuery(queryMethod, context, rowMapper);
5859
}
60+
61+
private RowMapper<?> determineDefaultRowMapper(JdbcQueryMethod queryMethod) {
62+
63+
Class<?> domainType = queryMethod.getReturnedObjectType();
64+
65+
RowMapper typeMappedRowMapper = rowMapperMap.rowMapperFor(domainType);
66+
67+
return typeMappedRowMapper == null //
68+
? new EntityRowMapper<>( //
69+
context.getRequiredPersistentEntity(domainType), //
70+
context.getConversions(), //
71+
context, //
72+
accessStrategy) //
73+
: typeMappedRowMapper;
74+
}
5975
}

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: 15 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,14 @@ 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, dataAccessStrategy);
67+
68+
if (rowMapperMap != null) {
69+
jdbcRepositoryFactory.setRowMapperMap(rowMapperMap);
70+
}
71+
72+
return jdbcRepositoryFactory;
6473
}
6574

6675
@Autowired
@@ -75,6 +84,11 @@ public void setDataAccessStrategy(DataAccessStrategy dataAccessStrategy) {
7584
this.dataAccessStrategy = dataAccessStrategy;
7685
}
7786

87+
@Autowired(required = false)
88+
public void setRowMapperMap(RowMapperMap rowMapperMap) {
89+
this.rowMapperMap = rowMapperMap;
90+
}
91+
7892
@Override
7993
public void afterPropertiesSet() {
8094

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
20+
21+
import org.junit.Test;
22+
import org.springframework.data.jdbc.repository.RowMapperMap;
23+
import org.springframework.jdbc.core.RowMapper;
24+
25+
/**
26+
* @author Jens Schauder
27+
*/
28+
public class ConfigurableRowMapperMapUnitTests {
29+
30+
@Test
31+
public void freshInstanceReturnsNull() {
32+
33+
RowMapperMap map = new ConfigurableRowMapperMap();
34+
35+
assertThat(map.rowMapperFor(Object.class)).isNull();
36+
}
37+
38+
@Test
39+
public void returnsConfiguredInstanceForClass() {
40+
41+
RowMapper rowMapper = mock(RowMapper.class);
42+
43+
RowMapperMap map = new ConfigurableRowMapperMap().register(Object.class, rowMapper);
44+
45+
assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper);
46+
}
47+
48+
@Test
49+
public void returnsNullForClassNotConfigured() {
50+
51+
RowMapper rowMapper = mock(RowMapper.class);
52+
53+
RowMapperMap map = new ConfigurableRowMapperMap().register(Number.class, rowMapper);
54+
55+
assertThat(map.rowMapperFor(Integer.class)).isNull();
56+
assertThat(map.rowMapperFor(String.class)).isNull();
57+
}
58+
59+
60+
@Test
61+
public void returnsInstanceRegisteredForSubClass() {
62+
63+
RowMapper rowMapper = mock(RowMapper.class);
64+
65+
RowMapperMap map = new ConfigurableRowMapperMap().register(String.class, rowMapper);
66+
67+
assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper);
68+
}
69+
70+
71+
@Test
72+
public void prefersExactTypeMatchClass() {
73+
74+
RowMapper rowMapper = mock(RowMapper.class);
75+
76+
RowMapperMap map = new ConfigurableRowMapperMap() //
77+
.register(Object.class, mock(RowMapper.class)) //
78+
.register(Integer.class, rowMapper) //
79+
.register(Number.class, mock(RowMapper.class));
80+
81+
assertThat(map.rowMapperFor(Integer.class)).isEqualTo(rowMapper);
82+
}
83+
84+
85+
@Test
86+
public void prefersLatestRegistrationForSuperTypeMatch() {
87+
88+
RowMapper rowMapper = mock(RowMapper.class);
89+
90+
RowMapperMap map = new ConfigurableRowMapperMap() //
91+
.register(Integer.class, mock(RowMapper.class)) //
92+
.register(Number.class, rowMapper);
93+
94+
assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper);
95+
}
96+
97+
}

0 commit comments

Comments
 (0)