Skip to content

Commit eb63036

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 60e22da commit eb63036

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

126155
@Test // gh-469
@@ -558,11 +587,15 @@ void updateExcludesInsertOnlyColumns() {
558587
record WithoutId(String name) {
559588
}
560589

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

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

565-
String description) {
598+
String description) implements Named {
566599

567600
public static Person empty() {
568601
return new Person(null, null, null);
@@ -579,6 +612,11 @@ public Person withName(String name) {
579612
public Person withDescription(String description) {
580613
return this.description == description ? this : new Person(this.id, this.name, description);
581614
}
615+
616+
@Override
617+
public String getName() {
618+
return name();
619+
}
582620
}
583621

584622
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)