Skip to content

Commit bca5b1b

Browse files
committed
Fix interface projection for entities that implement the interface.
Using as with an interface that is implemented by the entity, we no longer attempt to instantiate the interface bur use the entity type instead. Closes #1690
1 parent 40ba2c9 commit bca5b1b

File tree

3 files changed

+88
-18
lines changed

3 files changed

+88
-18
lines changed

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

+47-15
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@
2121
import reactor.core.publisher.Flux;
2222
import reactor.core.publisher.Mono;
2323

24-
import java.beans.FeatureDescriptor;
2524
import java.util.Collections;
25+
import java.util.LinkedHashSet;
2626
import java.util.List;
2727
import java.util.Map;
2828
import java.util.Optional;
29+
import java.util.Set;
2930
import java.util.function.BiFunction;
3031
import java.util.function.Function;
3132
import java.util.stream.Collectors;
3233

3334
import org.reactivestreams.Publisher;
35+
3436
import org.springframework.beans.BeansException;
3537
import org.springframework.beans.factory.BeanFactory;
3638
import org.springframework.beans.factory.BeanFactoryAware;
@@ -46,7 +48,6 @@
4648
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
4749
import org.springframework.data.mapping.context.MappingContext;
4850
import org.springframework.data.projection.EntityProjection;
49-
import org.springframework.data.projection.ProjectionInformation;
5051
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
5152
import org.springframework.data.r2dbc.convert.R2dbcConverter;
5253
import org.springframework.data.r2dbc.dialect.DialectResolver;
@@ -56,6 +57,7 @@
5657
import org.springframework.data.r2dbc.mapping.event.AfterSaveCallback;
5758
import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback;
5859
import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback;
60+
import org.springframework.data.relational.core.mapping.PersistentPropertyTranslator;
5961
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
6062
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
6163
import org.springframework.data.relational.core.query.Criteria;
@@ -68,6 +70,7 @@
6870
import org.springframework.data.relational.core.sql.SqlIdentifier;
6971
import org.springframework.data.relational.core.sql.Table;
7072
import org.springframework.data.relational.domain.RowDocument;
73+
import org.springframework.data.util.Predicates;
7174
import org.springframework.data.util.ProxyUtils;
7275
import org.springframework.lang.Nullable;
7376
import org.springframework.r2dbc.core.DatabaseClient;
@@ -332,7 +335,7 @@ private <T> RowsFetchSpec<T> doSelect(Query query, Class<?> entityType, SqlIdent
332335

333336
StatementMapper.SelectSpec selectSpec = statementMapper //
334337
.createSelect(tableName) //
335-
.doWithTable((table, spec) -> spec.withProjection(getSelectProjection(table, query, returnType)));
338+
.doWithTable((table, spec) -> spec.withProjection(getSelectProjection(table, query, entityType, returnType)));
336339

337340
if (query.getLimit() > 0) {
338341
selectSpec = selectSpec.limit(query.getLimit());
@@ -423,7 +426,8 @@ public <T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<T> entit
423426
}
424427

425428
@Override
426-
public <T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<?> entityClass, Class<T> resultType) throws DataAccessException {
429+
public <T> RowsFetchSpec<T> query(PreparedOperation<?> operation, Class<?> entityClass, Class<T> resultType)
430+
throws DataAccessException {
427431

428432
Assert.notNull(operation, "PreparedOperation must not be null");
429433
Assert.notNull(entityClass, "Entity class must not be null");
@@ -759,18 +763,16 @@ private <T> RelationalPersistentEntity<T> getRequiredEntity(T entity) {
759763
return (RelationalPersistentEntity) getRequiredEntity(entityType);
760764
}
761765

762-
private <T> List<Expression> getSelectProjection(Table table, Query query, Class<T> returnType) {
766+
private <T> List<Expression> getSelectProjection(Table table, Query query, Class<?> entityType, Class<T> returnType) {
763767

764768
if (query.getColumns().isEmpty()) {
765769

766-
if (returnType.isInterface()) {
770+
EntityProjection<T, ?> projection = converter.introspectProjection(returnType, entityType);
771+
772+
if (projection.isProjection() && projection.isClosedProjection()) {
767773

768-
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType);
774+
return computeProjectedFields(table, returnType, projection);
769775

770-
if (projectionInformation.isClosed()) {
771-
return projectionInformation.getInputProperties().stream().map(FeatureDescriptor::getName).map(table::column)
772-
.collect(Collectors.toList());
773-
}
774776
}
775777

776778
return Collections.singletonList(table.asterisk());
@@ -779,6 +781,36 @@ private <T> List<Expression> getSelectProjection(Table table, Query query, Class
779781
return query.getColumns().stream().map(table::column).collect(Collectors.toList());
780782
}
781783

784+
@SuppressWarnings("unchecked")
785+
private <T> List<Expression> computeProjectedFields(Table table, Class<T> returnType,
786+
EntityProjection<T, ?> projection) {
787+
788+
if (returnType.isInterface()) {
789+
790+
Set<String> properties = new LinkedHashSet<>();
791+
projection.forEach(it -> {
792+
properties.add(it.getPropertyPath().getSegment());
793+
});
794+
795+
return properties.stream().map(table::column).collect(Collectors.toList());
796+
}
797+
798+
Set<SqlIdentifier> properties = new LinkedHashSet<>();
799+
// DTO projections use merged metadata between domain type and result type
800+
PersistentPropertyTranslator translator = PersistentPropertyTranslator.create(
801+
mappingContext.getRequiredPersistentEntity(projection.getDomainType()),
802+
Predicates.negate(RelationalPersistentProperty::hasExplicitColumnName));
803+
804+
RelationalPersistentEntity<?> persistentEntity = mappingContext
805+
.getRequiredPersistentEntity(projection.getMappedType());
806+
for (RelationalPersistentProperty property : persistentEntity) {
807+
properties.add(translator.translate(property).getColumnName());
808+
}
809+
810+
return properties.stream().map(table::column).collect(Collectors.toList());
811+
}
812+
813+
@SuppressWarnings("unchecked")
782814
public <T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class<?> entityType,
783815
Class<T> resultType) {
784816

@@ -791,13 +823,13 @@ public <T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec e
791823
} else {
792824

793825
EntityProjection<T, ?> projection = converter.introspectProjection(resultType, entityType);
826+
Class<T> typeToRead = projection.isProjection() ? resultType
827+
: resultType.isInterface() ? (Class<T>) entityType : resultType;
794828

795829
rowMapper = (row, rowMetadata) -> {
796830

797-
RowDocument document = dataAccessStrategy.toRowDocument(resultType, row, rowMetadata.getColumnMetadatas());
798-
799-
return projection.isProjection() ? converter.project(projection, document)
800-
: converter.read(resultType, document);
831+
RowDocument document = dataAccessStrategy.toRowDocument(typeToRead, row, rowMetadata.getColumnMetadatas());
832+
return converter.project(projection, document);
801833
};
802834
}
803835

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java

+39-1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,35 @@ void shouldApplyInterfaceProjection() {
120120
.all() //
121121
.as(StepVerifier::create) //
122122
.assertNext(actual -> assertThat(actual.getName()).isEqualTo("Walter")).verifyComplete();
123+
124+
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT"));
125+
assertThat(statement.getSql()).isEqualTo("SELECT foo.THE_NAME FROM foo WHERE foo.THE_NAME = $1");
126+
}
127+
128+
@Test // GH-1690
129+
void shouldProjectEntityUsingInheritedInterface() {
130+
131+
MockRowMetadata metadata = MockRowMetadata.builder()
132+
.columnMetadata(MockColumnMetadata.builder().name("THE_NAME").type(R2dbcType.VARCHAR).build()).build();
133+
MockResult result = MockResult.builder()
134+
.row(MockRow.builder().identified("THE_NAME", Object.class, "Walter").metadata(metadata).build()).build();
135+
136+
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
137+
138+
entityTemplate.select(Person.class) //
139+
.from("foo") //
140+
.as(Named.class) //
141+
.matching(Query.query(Criteria.where("name").is("Walter"))) //
142+
.all() //
143+
.as(StepVerifier::create) //
144+
.assertNext(actual -> {
145+
assertThat(actual.getName()).isEqualTo("Walter");
146+
assertThat(actual).isInstanceOf(Person.class);
147+
}).verifyComplete();
148+
149+
150+
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT"));
151+
assertThat(statement.getSql()).isEqualTo("SELECT foo.* FROM foo WHERE foo.THE_NAME = $1");
123152
}
124153

125154
@Test // gh-469
@@ -557,11 +586,15 @@ void updateExcludesInsertOnlyColumns() {
557586
record WithoutId(String name) {
558587
}
559588

589+
interface Named{
590+
String getName();
591+
}
592+
560593
record Person(@Id String id,
561594

562595
@Column("THE_NAME") String name,
563596

564-
String description) {
597+
String description) implements Named {
565598

566599
public static Person empty() {
567600
return new Person(null, null, null);
@@ -578,6 +611,11 @@ public Person withName(String name) {
578611
public Person withDescription(String description) {
579612
return this.description == description ? this : new Person(this.id, this.name, description);
580613
}
614+
615+
@Override
616+
public String getName() {
617+
return name();
618+
}
581619
}
582620

583621
interface PersonProjection {

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ void shouldSelectAs() {
102102
assertThat(statement.getSql()).isEqualTo("SELECT person.THE_NAME FROM person WHERE person.THE_NAME = $1");
103103
}
104104

105-
@Test // gh-220
105+
@Test // GH-220, GH-1690
106106
void shouldSelectAsWithColumnName() {
107107

108108
MockRowMetadata metadata = MockRowMetadata.builder()
@@ -123,7 +123,7 @@ void shouldSelectAsWithColumnName() {
123123

124124
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT"));
125125

126-
assertThat(statement.getSql()).isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1");
126+
assertThat(statement.getSql()).isEqualTo("SELECT person.id, person.a_different_name FROM person WHERE person.THE_NAME = $1");
127127
}
128128

129129
@Test // gh-220

0 commit comments

Comments
 (0)