diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java index b0c51559d..fe13e598d 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java @@ -18,6 +18,8 @@ package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.Blob; import io.r2dbc.spi.Clob; import io.r2dbc.spi.ColumnMetadata; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -53,6 +55,7 @@ import org.springframework.data.relational.core.dialect.ArrayColumns; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.RowDocument; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.Parameter; @@ -75,7 +78,8 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements */ public MappingR2dbcConverter( MappingContext, ? extends RelationalPersistentProperty> context) { - super((RelationalMappingContext) context, new R2dbcCustomConversions(R2dbcCustomConversions.STORE_CONVERSIONS, Collections.emptyList())); + super((RelationalMappingContext) context, + new R2dbcCustomConversions(R2dbcCustomConversions.STORE_CONVERSIONS, Collections.emptyList())); } /** @@ -141,6 +145,54 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements return result; } + @Override + public RowDocument toRowDocument(Class type, Readable row, Iterable metadata) { + + RowDocument document = new RowDocument(); + RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type); + + if (persistentEntity != null) { + captureRowValues(row, metadata, document, persistentEntity); + } + + for (ReadableMetadata m : metadata) { + + if (document.containsKey(m.getName())) { + continue; + } + + document.put(m.getName(), row.get(m.getName())); + } + + return document; + } + + private static void captureRowValues(Readable row, Iterable metadata, + RowDocument document, RelationalPersistentEntity persistentEntity) { + + for (RelationalPersistentProperty property : persistentEntity) { + + String identifier = property.getColumnName().getReference(); + + if (property.isEntity() || !RowMetadataUtils.containsColumn(metadata, identifier)) { + continue; + } + + Object value; + Class propertyType = property.getType(); + + if (propertyType.equals(Clob.class)) { + value = row.get(identifier, Clob.class); + } else if (propertyType.equals(Blob.class)) { + value = row.get(identifier, Blob.class); + } else { + value = row.get(identifier); + } + + document.put(identifier, value); + } + } + /** * Read a single value or a complete Entity from the {@link Row} passed as an argument. * diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java index 7d062c315..a774641bb 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java @@ -15,6 +15,8 @@ */ package org.springframework.data.r2dbc.convert; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -29,6 +31,7 @@ import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.dialect.ArrayColumns; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.RowDocument; /** * Central R2DBC specific converter interface. @@ -103,4 +106,15 @@ public interface R2dbcConverter */ R read(Class type, Row source, RowMetadata metadata); + /** + * Create a flat {@link RowDocument} from a single {@link Readable Row or Stored Procedure output}. + * + * @param type the underlying entity type. + * @param row the row or stored procedure output to retrieve data from. + * @param metadata readable metadata. + * @return the {@link RowDocument} containing the data. + * @since 3.2 + */ + RowDocument toRowDocument(Class type, Readable row, Iterable metadata); + } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java index dfbb67b75..b4ea7dc1f 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java @@ -16,6 +16,7 @@ package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.ColumnMetadata; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.RowMetadata; /** @@ -34,10 +35,19 @@ class RowMetadataUtils { * @return {@code true} if the metadata contains the column {@code name}. */ public static boolean containsColumn(RowMetadata metadata, String name) { + return containsColumn(getColumnMetadata(metadata), name); + } - Iterable columns = getColumnMetadata(metadata); + /** + * Check whether the column {@code name} is contained in {@link RowMetadata}. The check happens case-insensitive. + * + * @param columns the metadata to inspect. + * @param name column name. + * @return {@code true} if the metadata contains the column {@code name}. + */ + public static boolean containsColumn(Iterable columns, String name) { - for (ColumnMetadata columnMetadata : columns) { + for (ReadableMetadata columnMetadata : columns) { if (name.equalsIgnoreCase(columnMetadata.getName())) { return true; } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java index 86eba3a71..4a066b6a1 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -15,6 +15,8 @@ */ package org.springframework.data.r2dbc.core; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -43,6 +45,7 @@ import org.springframework.data.relational.core.dialect.RenderContextFactory; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.relational.domain.RowDocument; import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.Parameter; import org.springframework.r2dbc.core.PreparedOperation; @@ -239,8 +242,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return Parameter.empty(targetArrayType); } - return Parameter.fromOrEmpty(this.converter.getArrayValue(arrayColumns, property, value.getValue()), - actualType); + return Parameter.fromOrEmpty(this.converter.getArrayValue(arrayColumns, property, value.getValue()), actualType); } @Override @@ -253,6 +255,11 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return new EntityRowMapper<>(typeToRead, this.converter); } + @Override + public RowDocument toRowDocument(Class type, Readable row, Iterable metadata) { + return this.converter.toRowDocument(type, row, metadata); + } + @Override public PreparedOperation processNamedParameters(String query, NamedParameterProvider parameterProvider) { @@ -289,6 +296,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return this.statementMapper; } + @Override public R2dbcConverter getConverter() { return this.converter; } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java index f88283fe5..68fa5e71f 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java @@ -45,6 +45,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.r2dbc.convert.R2dbcConverter; @@ -66,6 +67,7 @@ import org.springframework.data.relational.core.sql.Expressions; import org.springframework.data.relational.core.sql.Functions; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.domain.RowDocument; import org.springframework.data.util.ProxyUtils; import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.DatabaseClient; @@ -95,6 +97,8 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw private final ReactiveDataAccessStrategy dataAccessStrategy; + private final R2dbcConverter converter; + private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; private final SpelAwareProxyProjectionFactory projectionFactory; @@ -116,7 +120,8 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) .bindMarkers(dialect.getBindMarkersFactory()).build(); this.dataAccessStrategy = new DefaultReactiveDataAccessStrategy(dialect); - this.mappingContext = dataAccessStrategy.getConverter().getMappingContext(); + this.converter = dataAccessStrategy.getConverter(); + this.mappingContext = converter.getMappingContext(); this.projectionFactory = new SpelAwareProxyProjectionFactory(); } @@ -157,6 +162,7 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw this.databaseClient = databaseClient; this.dataAccessStrategy = strategy; + this.converter = dataAccessStrategy.getConverter(); this.mappingContext = strategy.getConverter().getMappingContext(); this.projectionFactory = new SpelAwareProxyProjectionFactory(); } @@ -173,7 +179,7 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw @Override public R2dbcConverter getConverter() { - return this.dataAccessStrategy.getConverter(); + return this.converter; } @Override @@ -334,10 +340,10 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw return (P) ((Flux) result).concatMap(it -> maybeCallAfterConvert(it, tableName)); } - private RowsFetchSpec doSelect(Query query, Class entityClass, SqlIdentifier tableName, + private RowsFetchSpec doSelect(Query query, Class entityType, SqlIdentifier tableName, Class returnType) { - StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); + StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityType); StatementMapper.SelectSpec selectSpec = statementMapper // .createSelect(tableName) // @@ -362,7 +368,7 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw PreparedOperation operation = statementMapper.getMappedObject(selectSpec); - return getRowsFetchSpec(databaseClient.sql(operation), entityClass, returnType); + return getRowsFetchSpec(databaseClient.sql(operation), entityType, returnType); } @Override @@ -783,19 +789,26 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw return query.getColumns().stream().map(table::column).collect(Collectors.toList()); } - private RowsFetchSpec getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class entityClass, - Class returnType) { + private RowsFetchSpec getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class entityType, + Class resultType) { - boolean simpleType; + boolean simpleType = getConverter().isSimpleType(resultType); BiFunction rowMapper; - if (returnType.isInterface()) { - simpleType = getConverter().isSimpleType(entityClass); - rowMapper = dataAccessStrategy.getRowMapper(entityClass) - .andThen(o -> projectionFactory.createProjection(returnType, o)); + + if (simpleType) { + rowMapper = dataAccessStrategy.getRowMapper(resultType); } else { - simpleType = getConverter().isSimpleType(returnType); - rowMapper = dataAccessStrategy.getRowMapper(returnType); + + EntityProjection projection = converter.introspectProjection(resultType, entityType); + + rowMapper = (row, rowMetadata) -> { + + RowDocument document = dataAccessStrategy.toRowDocument(resultType, row, rowMetadata.getColumnMetadatas()); + + return projection.isProjection() ? converter.project(projection, document) + : converter.read(resultType, document); + }; } // avoid top-level null values if the read type is a simple one (e.g. SELECT MAX(age) via Integer.class) diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java index b75cfb681..017b31129 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java @@ -15,6 +15,8 @@ */ package org.springframework.data.r2dbc.core; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -25,6 +27,7 @@ import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.mapping.OutboundRow; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.relational.domain.RowDocument; import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.Parameter; import org.springframework.r2dbc.core.PreparedOperation; @@ -82,6 +85,17 @@ public interface ReactiveDataAccessStrategy { */ BiFunction getRowMapper(Class typeToRead); + /** + * Create a flat {@link RowDocument} from a single {@link Readable Row or Stored Procedure output}. + * + * @param type the underlying entity type. + * @param row the row or stored procedure output to retrieve data from. + * @param metadata readable metadata. + * @return the {@link RowDocument} containing the data. + * @since 3.2 + */ + RowDocument toRowDocument(Class type, Readable row, Iterable metadata); + /** * @param type * @return the table name for the {@link Class entity type}. diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java index eb8c332c7..0d1541f32 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java @@ -28,7 +28,6 @@ import reactor.test.StepVerifier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.data.annotation.Id; import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.testing.StatementRecorder; @@ -103,6 +102,30 @@ public class ReactiveSelectOperationUnitTests { assertThat(statement.getSql()).isEqualTo("SELECT person.THE_NAME FROM person WHERE person.THE_NAME = $1"); } + @Test // gh-220 + void shouldSelectAsWithColumnName() { + + MockRowMetadata metadata = MockRowMetadata.builder() + .columnMetadata(MockColumnMetadata.builder().name("id").type(R2dbcType.INTEGER).build()) + .columnMetadata(MockColumnMetadata.builder().name("a_different_name").type(R2dbcType.VARCHAR).build()).build(); + MockResult result = MockResult.builder().row(MockRow.builder().identified("id", Object.class, "Walter") + .identified("a_different_name", Object.class, "Werner").metadata(metadata).build()).build(); + + recorder.addStubbing(s -> s.startsWith("SELECT"), result); + + entityTemplate.select(Person.class) // + .as(PersonProjectionWithColumnName.class) // + .matching(query(where("name").is("Walter"))) // + .all() // + .as(StepVerifier::create) // + .assertNext(actual -> assertThat(actual.getName()).isEqualTo("Werner")) // + .verifyComplete(); + + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); + + assertThat(statement.getSql()).isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1"); + } + @Test // gh-220 void shouldSelectFromTable() { @@ -234,6 +257,21 @@ public class ReactiveSelectOperationUnitTests { } } + static class PersonProjectionWithColumnName { + + @Id String id; + + @Column("a_different_name") String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + interface PersonProjection { String getName();