Skip to content

Commit b3a2925

Browse files
mp911deschauder
authored andcommitted
Add support for repository query method projections.
JDBC repository methods now support interface and DTO projections by specifying either the projection type as return type or using generics and providing a Class parameter to query methods. Closes #971 Original pull request #980
1 parent fc92bd5 commit b3a2925

File tree

12 files changed

+385
-98
lines changed

12 files changed

+385
-98
lines changed

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java

+59-9
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.query;
1717

18+
import java.sql.ResultSet;
19+
import java.sql.SQLException;
1820
import java.util.List;
1921

22+
import org.springframework.core.convert.converter.Converter;
2023
import org.springframework.dao.EmptyResultDataAccessException;
2124
import org.springframework.data.repository.query.RepositoryQuery;
25+
import org.springframework.data.repository.query.ResultProcessor;
26+
import org.springframework.data.repository.query.ReturnedType;
2227
import org.springframework.jdbc.core.ResultSetExtractor;
2328
import org.springframework.jdbc.core.RowMapper;
2429
import org.springframework.jdbc.core.RowMapperResultSetExtractor;
@@ -43,23 +48,17 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery {
4348
private final NamedParameterJdbcOperations operations;
4449

4550
/**
46-
* Creates a new {@link AbstractJdbcQuery} for the given {@link JdbcQueryMethod}, {@link NamedParameterJdbcOperations}
47-
* and {@link RowMapper}.
51+
* Creates a new {@link AbstractJdbcQuery} for the given {@link JdbcQueryMethod} and
52+
* {@link NamedParameterJdbcOperations}.
4853
*
4954
* @param queryMethod must not be {@literal null}.
5055
* @param operations must not be {@literal null}.
51-
* @param defaultRowMapper can be {@literal null} (only in case of a modifying query).
5256
*/
53-
AbstractJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
54-
@Nullable RowMapper<?> defaultRowMapper) {
57+
AbstractJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations) {
5558

5659
Assert.notNull(queryMethod, "Query method must not be null!");
5760
Assert.notNull(operations, "NamedParameterJdbcOperations must not be null!");
5861

59-
if (!queryMethod.isModifyingQuery()) {
60-
Assert.notNull(defaultRowMapper, "Mapper must not be null!");
61-
}
62-
6362
this.queryMethod = queryMethod;
6463
this.operations = operations;
6564
}
@@ -123,8 +122,59 @@ <T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> rowMapper) {
123122
return getQueryExecution(new RowMapperResultSetExtractor<>(rowMapper));
124123
}
125124

125+
/**
126+
* Obtain the result type to read from {@link ResultProcessor}.
127+
*
128+
* @param resultProcessor
129+
* @return
130+
*/
131+
protected Class<?> resolveTypeToRead(ResultProcessor resultProcessor) {
132+
133+
ReturnedType returnedType = resultProcessor.getReturnedType();
134+
135+
if (returnedType.getReturnedType().isAssignableFrom(returnedType.getDomainType())) {
136+
return returnedType.getDomainType();
137+
}
138+
// Slight deviation from R2DBC: Allow direct mapping into DTOs
139+
return returnedType.isProjecting() && returnedType.getReturnedType().isInterface() ? returnedType.getDomainType()
140+
: returnedType.getReturnedType();
141+
}
142+
126143
private <T> JdbcQueryExecution<T> getQueryExecution(ResultSetExtractor<T> resultSetExtractor) {
127144
return (query, parameters) -> operations.query(query, parameters, resultSetExtractor);
128145
}
129146

147+
/**
148+
* Factory to create a {@link RowMapper} for a given class.
149+
*
150+
* @since 2.3
151+
*/
152+
public interface RowMapperFactory {
153+
RowMapper<Object> create(Class<?> result);
154+
}
155+
156+
/**
157+
* Delegating {@link RowMapper} that reads a row into {@code T} and converts it afterwards into {@code Object}.
158+
*
159+
* @param <T>
160+
* @since 2.3
161+
*/
162+
protected static class ConvertingRowMapper<T> implements RowMapper<Object> {
163+
164+
private final RowMapper<T> delegate;
165+
private final Converter<Object, Object> converter;
166+
167+
public ConvertingRowMapper(RowMapper<T> delegate, Converter<Object, Object> converter) {
168+
this.delegate = delegate;
169+
this.converter = converter;
170+
}
171+
172+
@Override
173+
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
174+
175+
T object = delegate.mapRow(rs, rowNum);
176+
177+
return converter.convert(object);
178+
}
179+
}
130180
}

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.data.relational.core.sql.Table;
2828
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
2929
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
30+
import org.springframework.data.repository.query.ReturnedType;
3031
import org.springframework.data.repository.query.parser.PartTree;
3132

3233
/**
@@ -38,8 +39,9 @@
3839
class JdbcCountQueryCreator extends JdbcQueryCreator {
3940

4041
JdbcCountQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
41-
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery) {
42-
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery);
42+
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
43+
ReturnedType returnedType) {
44+
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType);
4345
}
4446

4547
@Override

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
4545
import org.springframework.data.relational.repository.query.RelationalQueryCreator;
4646
import org.springframework.data.repository.query.Parameters;
47+
import org.springframework.data.repository.query.ReturnedType;
4748
import org.springframework.data.repository.query.parser.Part;
4849
import org.springframework.data.repository.query.parser.PartTree;
4950
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
@@ -67,6 +68,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
6768
private final RelationalEntityMetadata<?> entityMetadata;
6869
private final RenderContextFactory renderContextFactory;
6970
private final boolean isSliceQuery;
71+
private final ReturnedType returnedType;
7072

7173
/**
7274
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
@@ -79,14 +81,17 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
7981
* @param entityMetadata relational entity metadata, must not be {@literal null}.
8082
* @param accessor parameter metadata provider, must not be {@literal null}.
8183
* @param isSliceQuery
84+
* @param returnedType
8285
*/
8386
JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
84-
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery) {
87+
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
88+
ReturnedType returnedType) {
8589
super(tree, accessor);
8690

8791
Assert.notNull(converter, "JdbcConverter must not be null");
8892
Assert.notNull(dialect, "Dialect must not be null");
8993
Assert.notNull(entityMetadata, "Relational entity metadata must not be null");
94+
Assert.notNull(returnedType, "ReturnedType must not be null");
9095

9196
this.context = context;
9297
this.tree = tree;
@@ -96,6 +101,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
96101
this.queryMapper = new QueryMapper(dialect, converter);
97102
this.renderContextFactory = new RenderContextFactory(dialect);
98103
this.isSliceQuery = isSliceQuery;
104+
this.returnedType = returnedType;
99105
}
100106

101107
/**
@@ -241,6 +247,13 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) {
241247
joinTables.add(join);
242248
}
243249

250+
if (returnedType.needsCustomConstruction()) {
251+
if (!returnedType.getInputProperties()
252+
.contains(extPath.getRequiredPersistentPropertyPath().getBaseProperty().getName())) {
253+
continue;
254+
}
255+
}
256+
244257
Column column = getColumn(sqlContext, extPath);
245258
if (column != null) {
246259
columnExpressions.add(column);

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java

+47
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.query;
1717

18+
import org.springframework.core.convert.converter.Converter;
19+
import org.springframework.data.mapping.context.MappingContext;
20+
import org.springframework.data.mapping.model.EntityInstantiators;
21+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
22+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
23+
import org.springframework.data.relational.repository.query.DtoInstantiatingConverter;
24+
import org.springframework.data.repository.query.ResultProcessor;
25+
import org.springframework.data.repository.query.ReturnedType;
26+
import org.springframework.data.util.Lazy;
1827
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
1928
import org.springframework.lang.Nullable;
29+
import org.springframework.util.ClassUtils;
2030

2131
/**
2232
* Interface specifying a query execution strategy. Implementations encapsulate information how to actually execute the
@@ -37,4 +47,41 @@ interface JdbcQueryExecution<T> {
3747
*/
3848
@Nullable
3949
T execute(String query, SqlParameterSource parameter);
50+
51+
/**
52+
* A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}.
53+
*
54+
* @author Mark Paluch
55+
* @since 2.3
56+
*/
57+
class ResultProcessingConverter implements Converter<Object, Object> {
58+
59+
private final ResultProcessor processor;
60+
private final Lazy<Converter<Object, Object>> converter;
61+
62+
ResultProcessingConverter(ResultProcessor processor,
63+
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext,
64+
EntityInstantiators instantiators) {
65+
this.processor = processor;
66+
this.converter = Lazy.of(() -> new DtoInstantiatingConverter(processor.getReturnedType().getReturnedType(),
67+
mappingContext, instantiators));
68+
}
69+
70+
/*
71+
* (non-Javadoc)
72+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
73+
*/
74+
@Override
75+
public Object convert(Object source) {
76+
77+
ReturnedType returnedType = processor.getReturnedType();
78+
79+
if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())
80+
|| returnedType.getReturnedType().isInstance(source)) {
81+
return source;
82+
}
83+
84+
return processor.processResult(source, converter.get());
85+
}
86+
}
4087
}

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java

+55-19
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.query;
1717

18+
import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.*;
19+
1820
import java.sql.ResultSet;
1921
import java.util.ArrayList;
2022
import java.util.Collection;
2123
import java.util.List;
2224
import java.util.function.LongSupplier;
2325

26+
import org.springframework.core.convert.converter.Converter;
2427
import org.springframework.data.domain.Pageable;
2528
import org.springframework.data.domain.Slice;
2629
import org.springframework.data.domain.SliceImpl;
@@ -32,6 +35,8 @@
3235
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
3336
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
3437
import org.springframework.data.repository.query.Parameters;
38+
import org.springframework.data.repository.query.ResultProcessor;
39+
import org.springframework.data.repository.query.ReturnedType;
3540
import org.springframework.data.repository.query.parser.PartTree;
3641
import org.springframework.data.support.PageableExecutionUtils;
3742
import org.springframework.jdbc.core.ResultSetExtractor;
@@ -53,9 +58,8 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
5358
private final Parameters<?, ?> parameters;
5459
private final Dialect dialect;
5560
private final JdbcConverter converter;
61+
private final RowMapperFactory rowMapperFactory;
5662
private final PartTree tree;
57-
/** The execution for obtaining the bulk of the data. The execution may be decorated with further processing for handling sliced or paged queries */
58-
private final JdbcQueryExecution<?> coreExecution;
5963

6064
/**
6165
* Creates a new {@link PartTreeJdbcQuery}.
@@ -69,26 +73,40 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
6973
*/
7074
public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod queryMethod, Dialect dialect,
7175
JdbcConverter converter, NamedParameterJdbcOperations operations, RowMapper<Object> rowMapper) {
76+
this(context, queryMethod, dialect, converter, operations, it -> rowMapper);
77+
}
78+
79+
/**
80+
* Creates a new {@link PartTreeJdbcQuery}.
81+
*
82+
* @param context must not be {@literal null}.
83+
* @param queryMethod must not be {@literal null}.
84+
* @param dialect must not be {@literal null}.
85+
* @param converter must not be {@literal null}.
86+
* @param operations must not be {@literal null}.
87+
* @param rowMapperFactory must not be {@literal null}.
88+
* @since 2.3
89+
*/
90+
public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod queryMethod, Dialect dialect,
91+
JdbcConverter converter, NamedParameterJdbcOperations operations, RowMapperFactory rowMapperFactory) {
7292

73-
super(queryMethod, operations, rowMapper);
93+
super(queryMethod, operations);
7494

7595
Assert.notNull(context, "RelationalMappingContext must not be null");
7696
Assert.notNull(queryMethod, "JdbcQueryMethod must not be null");
7797
Assert.notNull(dialect, "Dialect must not be null");
7898
Assert.notNull(converter, "JdbcConverter must not be null");
99+
Assert.notNull(rowMapperFactory, "RowMapperFactory must not be null");
79100

80101
this.context = context;
81102
this.parameters = queryMethod.getParameters();
82103
this.dialect = dialect;
83104
this.converter = converter;
105+
this.rowMapperFactory = rowMapperFactory;
84106

85107
this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType());
86108
JdbcQueryCreator.validate(this.tree, this.parameters, this.converter.getMappingContext());
87109

88-
ResultSetExtractor<Boolean> extractor = tree.isExistsProjection() ? (ResultSet::next) : null;
89-
90-
this.coreExecution = queryMethod.isPageQuery() || queryMethod.isSliceQuery() ? collectionQuery(rowMapper)
91-
: getQueryExecution(queryMethod, extractor, rowMapper);
92110
}
93111

94112
private Sort getDynamicSort(RelationalParameterAccessor accessor) {
@@ -104,30 +122,48 @@ public Object execute(Object[] values) {
104122

105123
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
106124
values);
107-
ParametrizedQuery query = createQuery(accessor);
108-
JdbcQueryExecution<?> execution = getDecoratedExecution(accessor);
125+
126+
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
127+
ParametrizedQuery query = createQuery(accessor, processor.getReturnedType());
128+
JdbcQueryExecution<?> execution = getQueryExecution(processor, accessor);
109129

110130
return execution.execute(query.getQuery(), query.getParameterSource());
111131
}
112132

113-
/**
114-
* The decorated execution is the {@link #coreExecution} decorated with further processing for handling sliced or paged queries.
115-
*/
116-
private JdbcQueryExecution<?> getDecoratedExecution(RelationalParametersParameterAccessor accessor) {
133+
private JdbcQueryExecution<?> getQueryExecution(ResultProcessor processor,
134+
RelationalParametersParameterAccessor accessor) {
135+
136+
ResultSetExtractor<Boolean> extractor = tree.isExistsProjection() ? (ResultSet::next) : null;
137+
138+
RowMapper<Object> rowMapper;
139+
140+
if (tree.isCountProjection() || tree.isExistsProjection()) {
141+
rowMapper = rowMapperFactory.create(resolveTypeToRead(processor));
142+
} else {
143+
144+
Converter<Object, Object> resultProcessingConverter = new ResultProcessingConverter(processor,
145+
this.converter.getMappingContext(), this.converter.getEntityInstantiators());
146+
rowMapper = new ConvertingRowMapper<>(rowMapperFactory.create(processor.getReturnedType().getDomainType()),
147+
resultProcessingConverter);
148+
}
149+
150+
JdbcQueryExecution<?> queryExecution = getQueryMethod().isPageQuery() || getQueryMethod().isSliceQuery()
151+
? collectionQuery(rowMapper)
152+
: getQueryExecution(getQueryMethod(), extractor, rowMapper);
117153

118154
if (getQueryMethod().isSliceQuery()) {
119-
return new SliceQueryExecution<>((JdbcQueryExecution<Collection<Object>>) this.coreExecution, accessor.getPageable());
155+
return new SliceQueryExecution<>((JdbcQueryExecution<Collection<Object>>) queryExecution, accessor.getPageable());
120156
}
121157

122158
if (getQueryMethod().isPageQuery()) {
123159

124-
return new PageQueryExecution<>((JdbcQueryExecution<Collection<Object>>) this.coreExecution, accessor.getPageable(),
160+
return new PageQueryExecution<>((JdbcQueryExecution<Collection<Object>>) queryExecution, accessor.getPageable(),
125161
() -> {
126162

127163
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
128164

129165
JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator(context, tree, converter, dialect,
130-
entityMetadata, accessor, false);
166+
entityMetadata, accessor, false, processor.getReturnedType());
131167

132168
ParametrizedQuery countQuery = queryCreator.createQuery(Sort.unsorted());
133169
Object count = singleObjectQuery((rs, i) -> rs.getLong(1)).execute(countQuery.getQuery(),
@@ -137,15 +173,15 @@ private JdbcQueryExecution<?> getDecoratedExecution(RelationalParametersParamete
137173
});
138174
}
139175

140-
return this.coreExecution;
176+
return queryExecution;
141177
}
142178

143-
protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor) {
179+
protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor, ReturnedType returnedType) {
144180

145181
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
146182

147183
JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor,
148-
getQueryMethod().isSliceQuery());
184+
getQueryMethod().isSliceQuery(), returnedType);
149185
return queryCreator.createQuery(getDynamicSort(accessor));
150186
}
151187

0 commit comments

Comments
 (0)