Skip to content

Commit 61a1451

Browse files
committed
Use Converter-based projection for R2DBC repository queries.
Previously, we instantiated the underlying entity. Now, we either read results directly into the result type or use a Map-backed projection. Closes #1687
1 parent 343569b commit 61a1451

File tree

6 files changed

+216
-14
lines changed

6 files changed

+216
-14
lines changed

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityOperations.java

+19
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,22 @@ public interface R2dbcEntityOperations extends FluentR2dbcOperations {
159159
*/
160160
<T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<T> entityClass) throws DataAccessException;
161161

162+
/**
163+
* Execute a query for a {@link RowsFetchSpec}, given {@link PreparedOperation}. Any provided bindings within
164+
* {@link PreparedOperation} are applied to the underlying {@link DatabaseClient}. The query is issued as-is without
165+
* additional pre-processing such as named parameter expansion. Results of the query are mapped onto
166+
* {@code entityClass}.
167+
*
168+
* @param operation the prepared operation wrapping a SQL query and bind parameters.
169+
* @param entityClass the entity type must not be {@literal null}.
170+
* @param resultType the returned entity, type must not be {@literal null}.
171+
* @return a {@link RowsFetchSpec} ready to materialize.
172+
* @throws DataAccessException if there is any problem issuing the execution.
173+
* @since 3.2.1
174+
*/
175+
<T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<?> entityClass, Class<T> resultType)
176+
throws DataAccessException;
177+
162178
/**
163179
* Execute a query for a {@link RowsFetchSpec}, given {@link PreparedOperation}. Any provided bindings within
164180
* {@link PreparedOperation} are applied to the underlying {@link DatabaseClient}. The query is issued as-is without
@@ -234,6 +250,9 @@ default <T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<?> enti
234250
<T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<?> entityClass,
235251
BiFunction<Row, RowMetadata, T> rowMapper) throws DataAccessException;
236252

253+
<T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class<?> entityType,
254+
Class<T> resultType);
255+
237256
// -------------------------------------------------------------------------
238257
// Methods dealing with entities
239258
// -------------------------------------------------------------------------

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -419,11 +419,16 @@ Mono<Long> doDelete(Query query, Class<?> entityClass, SqlIdentifier tableName)
419419

420420
@Override
421421
public <T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<T> entityClass) {
422+
return query(operation, entityClass, entityClass);
423+
}
424+
425+
@Override
426+
public <T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<?> entityClass, Class<T> resultType) throws DataAccessException {
422427

423428
Assert.notNull(operation, "PreparedOperation must not be null");
424429
Assert.notNull(entityClass, "Entity class must not be null");
425430

426-
return new EntityCallbackAdapter<>(getRowsFetchSpec(databaseClient.sql(operation), entityClass, entityClass),
431+
return new EntityCallbackAdapter<>(getRowsFetchSpec(databaseClient.sql(operation), entityClass, resultType),
427432
getTableNameOrEmpty(entityClass));
428433
}
429434

@@ -774,7 +779,7 @@ private <T> List<Expression> getSelectProjection(Table table, Query query, Class
774779
return query.getColumns().stream().map(table::column).collect(Collectors.toList());
775780
}
776781

777-
private <T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class<?> entityType,
782+
public <T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class<?> entityType,
778783
Class<T> resultType) {
779784

780785
boolean simpleType = getConverter().isSimpleType(resultType);

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java

+10-8
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,15 @@ private Publisher<?> executeQuery(R2dbcParameterAccessor parameterAccessor, Prep
9090
} else if (isExistsQuery()) {
9191
fetchSpec = entityOperations.getDatabaseClient().sql(operation).map(row -> true);
9292
} else {
93-
fetchSpec = entityOperations.query(operation, resolveResultType(processor));
93+
fetchSpec = entityOperations.query(operation, processor.getReturnedType()
94+
.getDomainType(),
95+
resolveResultType(processor));
9496
}
9597

9698
R2dbcQueryExecution execution = new ResultProcessingExecution(getExecutionToWrap(processor.getReturnedType()),
9799
new ResultProcessingConverter(processor, converter.getMappingContext(), instantiators));
98100

99-
return execution.execute(RowsFetchSpec.class.cast(fetchSpec));
101+
return execution.execute((RowsFetchSpec) fetchSpec);
100102
}
101103

102104
Class<?> resolveResultType(ResultProcessor resultProcessor) {
@@ -107,7 +109,7 @@ Class<?> resolveResultType(ResultProcessor resultProcessor) {
107109
return returnedType.getDomainType();
108110
}
109111

110-
return returnedType.isProjecting() ? returnedType.getDomainType() : returnedType.getReturnedType();
112+
return returnedType.getReturnedType();
111113
}
112114

113115
private R2dbcQueryExecution getExecutionToWrap(ReturnedType returnedType) {
@@ -122,17 +124,17 @@ private R2dbcQueryExecution getExecutionToWrap(ReturnedType returnedType) {
122124

123125
if (Boolean.class.isAssignableFrom(returnedType.getReturnedType())) {
124126
return fs.rowsUpdated().map(integer -> integer > 0);
125-
}
127+
}
126128

127-
if (Number.class.isAssignableFrom(returnedType.getReturnedType())) {
129+
if (Number.class.isAssignableFrom(returnedType.getReturnedType())) {
128130

129131
return fs.rowsUpdated()
130132
.map(count -> converter.getConversionService().convert(count, returnedType.getReturnedType()));
131-
}
133+
}
132134

133-
if (ReflectionUtils.isVoid(returnedType.getReturnedType())) {
135+
if (ReflectionUtils.isVoid(returnedType.getReturnedType())) {
134136
return fs.rowsUpdated().then();
135-
}
137+
}
136138

137139
return fs.rowsUpdated();
138140
};

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ protected ConnectionFactory createConnectionFactory() {
105105
}
106106

107107
@Test
108-
public void shouldInsertAndReadItems() {
108+
void shouldInsertAndReadItems() {
109109

110110
ConvertedEntity entity = new ConvertedEntity();
111111
entity.name = "name";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright 2019-2023 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+
* https://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.r2dbc.repository;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import io.r2dbc.mssql.util.Assert;
21+
import io.r2dbc.spi.ConnectionFactory;
22+
import reactor.core.publisher.Flux;
23+
import reactor.test.StepVerifier;
24+
25+
import javax.sql.DataSource;
26+
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.extension.ExtendWith;
30+
31+
import org.springframework.beans.factory.annotation.Autowired;
32+
import org.springframework.context.annotation.ComponentScan;
33+
import org.springframework.context.annotation.Configuration;
34+
import org.springframework.context.annotation.FilterType;
35+
import org.springframework.dao.DataAccessException;
36+
import org.springframework.data.annotation.Id;
37+
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
38+
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
39+
import org.springframework.data.r2dbc.testing.H2TestSupport;
40+
import org.springframework.data.relational.core.mapping.Table;
41+
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
42+
import org.springframework.jdbc.core.JdbcTemplate;
43+
import org.springframework.lang.Nullable;
44+
import org.springframework.test.context.junit.jupiter.SpringExtension;
45+
46+
/**
47+
* Integration tests projections.
48+
*
49+
* @author Mark Paluch
50+
*/
51+
@ExtendWith(SpringExtension.class)
52+
public class ProjectingRepositoryIntegrationTests {
53+
54+
@Autowired
55+
private ImmutableObjectRepository repository;
56+
private JdbcTemplate jdbc;
57+
58+
@Configuration
59+
@EnableR2dbcRepositories(
60+
includeFilters = @ComponentScan.Filter(value = ImmutableObjectRepository.class, type = FilterType.ASSIGNABLE_TYPE),
61+
considerNestedRepositories = true)
62+
static class TestConfiguration extends AbstractR2dbcConfiguration {
63+
@Override
64+
public ConnectionFactory connectionFactory() {
65+
return H2TestSupport.createConnectionFactory();
66+
}
67+
68+
}
69+
70+
@BeforeEach
71+
void before() {
72+
73+
this.jdbc = new JdbcTemplate(createDataSource());
74+
75+
try {
76+
this.jdbc.execute("DROP TABLE immutable_non_null");
77+
}
78+
catch (DataAccessException e) {
79+
}
80+
81+
this.jdbc.execute("CREATE TABLE immutable_non_null (id serial PRIMARY KEY, name varchar(255), email varchar(255))");
82+
this.jdbc.execute("INSERT INTO immutable_non_null VALUES (42, 'Walter', '[email protected]')");
83+
}
84+
85+
/**
86+
* Creates a {@link DataSource} to be used in this test.
87+
*
88+
* @return the {@link DataSource} to be used in this test.
89+
*/
90+
protected DataSource createDataSource() {
91+
return H2TestSupport.createDataSource();
92+
}
93+
94+
/**
95+
* Creates a {@link ConnectionFactory} to be used in this test.
96+
*
97+
* @return the {@link ConnectionFactory} to be used in this test.
98+
*/
99+
protected ConnectionFactory createConnectionFactory() {
100+
return H2TestSupport.createConnectionFactory();
101+
}
102+
103+
@Test
104+
// GH-1687
105+
void shouldApplyProjectionDirectly() {
106+
107+
repository.findProjectionByEmail("[email protected]") //
108+
.as(StepVerifier::create) //
109+
.consumeNextWith(actual -> {
110+
assertThat(actual.getName()).isEqualTo("Walter");
111+
}).verifyComplete();
112+
}
113+
114+
@Test
115+
// GH-1687
116+
void shouldApplyEntityQueryProjectionDirectly() {
117+
118+
repository.findAllByEmail("[email protected]") //
119+
.as(StepVerifier::create) //
120+
.consumeNextWith(actual -> {
121+
assertThat(actual.getName()).isEqualTo("Walter");
122+
assertThat(actual).isInstanceOf(ImmutableNonNullEntity.class);
123+
}).verifyComplete();
124+
}
125+
126+
interface ImmutableObjectRepository extends ReactiveCrudRepository<ImmutableNonNullEntity, Integer> {
127+
128+
Flux<ProjectionOnNonNull> findProjectionByEmail(String email);
129+
130+
Flux<Person> findAllByEmail(String email);
131+
132+
}
133+
134+
@Table("immutable_non_null")
135+
static class ImmutableNonNullEntity implements Person {
136+
137+
final @Nullable
138+
@Id Integer id;
139+
final String name;
140+
final String email;
141+
142+
ImmutableNonNullEntity(@Nullable Integer id, String name, String email) {
143+
144+
Assert.notNull(name, "Name must not be null");
145+
Assert.notNull(email, "Email must not be null");
146+
147+
this.id = id;
148+
this.name = name;
149+
this.email = email;
150+
}
151+
152+
@Override
153+
public String getName() {
154+
return name;
155+
}
156+
}
157+
158+
interface Person {
159+
160+
String getName();
161+
162+
}
163+
164+
interface ProjectionOnNonNull {
165+
166+
String getName();
167+
168+
}
169+
170+
}

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,18 @@ void translatesEnumToDatabaseValue() {
266266
verifyNoMoreInteractions(bindTarget);
267267
}
268268

269-
@Test // gh-475
270-
void usesDomainTypeForInterfaceProjectionResultMapping() {
269+
@Test
270+
// gh-475, GH-1687
271+
void usesProjectionTypeForInterfaceProjectionResultMapping() {
271272

272273
StringBasedR2dbcQuery query = getQueryMethod("findAsInterfaceProjection");
273274

274-
assertThat(query.resolveResultType(query.getQueryMethod().getResultProcessor())).isEqualTo(Person.class);
275+
assertThat(query.getQueryMethod().getResultProcessor().getReturnedType()
276+
.getReturnedType()).isEqualTo(PersonProjection.class);
277+
assertThat(query.getQueryMethod().getResultProcessor().getReturnedType()
278+
.getDomainType()).isEqualTo(Person.class);
279+
assertThat(query.resolveResultType(query.getQueryMethod()
280+
.getResultProcessor())).isEqualTo(PersonProjection.class);
275281
}
276282

277283
@Test // gh-475

0 commit comments

Comments
 (0)