Skip to content

Commit e070d97

Browse files
mp911deschauder
authored andcommitted
Migrate R2DBC to read RowDocument.
1 parent 6b8ecbe commit e070d97

File tree

7 files changed

+169
-20
lines changed

7 files changed

+169
-20
lines changed

Diff for: spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java

+53-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import io.r2dbc.spi.Blob;
1919
import io.r2dbc.spi.Clob;
2020
import io.r2dbc.spi.ColumnMetadata;
21+
import io.r2dbc.spi.Readable;
22+
import io.r2dbc.spi.ReadableMetadata;
2123
import io.r2dbc.spi.Row;
2224
import io.r2dbc.spi.RowMetadata;
2325

@@ -53,6 +55,7 @@
5355
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
5456
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
5557
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
58+
import org.springframework.data.relational.domain.RowDocument;
5659
import org.springframework.data.util.TypeInformation;
5760
import org.springframework.lang.Nullable;
5861
import org.springframework.r2dbc.core.Parameter;
@@ -75,7 +78,8 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements
7578
*/
7679
public MappingR2dbcConverter(
7780
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context) {
78-
super((RelationalMappingContext) context, new R2dbcCustomConversions(R2dbcCustomConversions.STORE_CONVERSIONS, Collections.emptyList()));
81+
super((RelationalMappingContext) context,
82+
new R2dbcCustomConversions(R2dbcCustomConversions.STORE_CONVERSIONS, Collections.emptyList()));
7983
}
8084

8185
/**
@@ -141,6 +145,54 @@ private <R> R read(RelationalPersistentEntity<R> entity, Row row, @Nullable RowM
141145
return result;
142146
}
143147

148+
@Override
149+
public RowDocument toRowDocument(Class<?> type, Readable row, Iterable<? extends ReadableMetadata> metadata) {
150+
151+
RowDocument document = new RowDocument();
152+
RelationalPersistentEntity<?> persistentEntity = getMappingContext().getPersistentEntity(type);
153+
154+
if (persistentEntity != null) {
155+
captureRowValues(row, metadata, document, persistentEntity);
156+
}
157+
158+
for (ReadableMetadata m : metadata) {
159+
160+
if (document.containsKey(m.getName())) {
161+
continue;
162+
}
163+
164+
document.put(m.getName(), row.get(m.getName()));
165+
}
166+
167+
return document;
168+
}
169+
170+
private static void captureRowValues(Readable row, Iterable<? extends ReadableMetadata> metadata,
171+
RowDocument document, RelationalPersistentEntity<?> persistentEntity) {
172+
173+
for (RelationalPersistentProperty property : persistentEntity) {
174+
175+
String identifier = property.getColumnName().getReference();
176+
177+
if (property.isEntity() || !RowMetadataUtils.containsColumn(metadata, identifier)) {
178+
continue;
179+
}
180+
181+
Object value;
182+
Class<?> propertyType = property.getType();
183+
184+
if (propertyType.equals(Clob.class)) {
185+
value = row.get(identifier, Clob.class);
186+
} else if (propertyType.equals(Blob.class)) {
187+
value = row.get(identifier, Blob.class);
188+
} else {
189+
value = row.get(identifier);
190+
}
191+
192+
document.put(identifier, value);
193+
}
194+
}
195+
144196
/**
145197
* Read a single value or a complete Entity from the {@link Row} passed as an argument.
146198
*

Diff for: spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java

+14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.r2dbc.convert;
1717

18+
import io.r2dbc.spi.Readable;
19+
import io.r2dbc.spi.ReadableMetadata;
1820
import io.r2dbc.spi.Row;
1921
import io.r2dbc.spi.RowMetadata;
2022

@@ -29,6 +31,7 @@
2931
import org.springframework.data.relational.core.dialect.ArrayColumns;
3032
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3133
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
34+
import org.springframework.data.relational.domain.RowDocument;
3235

3336
/**
3437
* Central R2DBC specific converter interface.
@@ -103,4 +106,15 @@ public interface R2dbcConverter
103106
*/
104107
<R> R read(Class<R> type, Row source, RowMetadata metadata);
105108

109+
/**
110+
* Create a flat {@link RowDocument} from a single {@link Readable Row or Stored Procedure output}.
111+
*
112+
* @param type the underlying entity type.
113+
* @param row the row or stored procedure output to retrieve data from.
114+
* @param metadata readable metadata.
115+
* @return the {@link RowDocument} containing the data.
116+
* @since 3.2
117+
*/
118+
RowDocument toRowDocument(Class<?> type, Readable row, Iterable<? extends ReadableMetadata> metadata);
119+
106120
}

Diff for: spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.r2dbc.convert;
1717

1818
import io.r2dbc.spi.ColumnMetadata;
19+
import io.r2dbc.spi.ReadableMetadata;
1920
import io.r2dbc.spi.RowMetadata;
2021

2122
/**
@@ -34,10 +35,19 @@ class RowMetadataUtils {
3435
* @return {@code true} if the metadata contains the column {@code name}.
3536
*/
3637
public static boolean containsColumn(RowMetadata metadata, String name) {
38+
return containsColumn(getColumnMetadata(metadata), name);
39+
}
3740

38-
Iterable<? extends ColumnMetadata> columns = getColumnMetadata(metadata);
41+
/**
42+
* Check whether the column {@code name} is contained in {@link RowMetadata}. The check happens case-insensitive.
43+
*
44+
* @param columns the metadata to inspect.
45+
* @param name column name.
46+
* @return {@code true} if the metadata contains the column {@code name}.
47+
*/
48+
public static boolean containsColumn(Iterable<? extends ReadableMetadata> columns, String name) {
3949

40-
for (ColumnMetadata columnMetadata : columns) {
50+
for (ReadableMetadata columnMetadata : columns) {
4151
if (name.equalsIgnoreCase(columnMetadata.getName())) {
4252
return true;
4353
}

Diff for: spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.r2dbc.core;
1717

18+
import io.r2dbc.spi.Readable;
19+
import io.r2dbc.spi.ReadableMetadata;
1820
import io.r2dbc.spi.Row;
1921
import io.r2dbc.spi.RowMetadata;
2022

@@ -43,6 +45,7 @@
4345
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
4446
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
4547
import org.springframework.data.relational.core.sql.SqlIdentifier;
48+
import org.springframework.data.relational.domain.RowDocument;
4649
import org.springframework.lang.Nullable;
4750
import org.springframework.r2dbc.core.Parameter;
4851
import org.springframework.r2dbc.core.PreparedOperation;
@@ -239,8 +242,7 @@ private Parameter getArrayValue(Parameter value, RelationalPersistentProperty pr
239242
return Parameter.empty(targetArrayType);
240243
}
241244

242-
return Parameter.fromOrEmpty(this.converter.getArrayValue(arrayColumns, property, value.getValue()),
243-
actualType);
245+
return Parameter.fromOrEmpty(this.converter.getArrayValue(arrayColumns, property, value.getValue()), actualType);
244246
}
245247

246248
@Override
@@ -253,6 +255,11 @@ public <T> BiFunction<Row, RowMetadata, T> getRowMapper(Class<T> typeToRead) {
253255
return new EntityRowMapper<>(typeToRead, this.converter);
254256
}
255257

258+
@Override
259+
public RowDocument toRowDocument(Class<?> type, Readable row, Iterable<? extends ReadableMetadata> metadata) {
260+
return this.converter.toRowDocument(type, row, metadata);
261+
}
262+
256263
@Override
257264
public PreparedOperation<?> processNamedParameters(String query, NamedParameterProvider parameterProvider) {
258265

@@ -289,6 +296,7 @@ public StatementMapper getStatementMapper() {
289296
return this.statementMapper;
290297
}
291298

299+
@Override
292300
public R2dbcConverter getConverter() {
293301
return this.converter;
294302
}

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

+27-14
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.springframework.data.mapping.PersistentPropertyAccessor;
4646
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
4747
import org.springframework.data.mapping.context.MappingContext;
48+
import org.springframework.data.projection.EntityProjection;
4849
import org.springframework.data.projection.ProjectionInformation;
4950
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
5051
import org.springframework.data.r2dbc.convert.R2dbcConverter;
@@ -66,6 +67,7 @@
6667
import org.springframework.data.relational.core.sql.Functions;
6768
import org.springframework.data.relational.core.sql.SqlIdentifier;
6869
import org.springframework.data.relational.core.sql.Table;
70+
import org.springframework.data.relational.domain.RowDocument;
6971
import org.springframework.data.util.ProxyUtils;
7072
import org.springframework.lang.Nullable;
7173
import org.springframework.r2dbc.core.DatabaseClient;
@@ -95,6 +97,8 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
9597

9698
private final ReactiveDataAccessStrategy dataAccessStrategy;
9799

100+
private final R2dbcConverter converter;
101+
98102
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
99103

100104
private final SpelAwareProxyProjectionFactory projectionFactory;
@@ -116,7 +120,8 @@ public R2dbcEntityTemplate(ConnectionFactory connectionFactory) {
116120
this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory)
117121
.bindMarkers(dialect.getBindMarkersFactory()).build();
118122
this.dataAccessStrategy = new DefaultReactiveDataAccessStrategy(dialect);
119-
this.mappingContext = dataAccessStrategy.getConverter().getMappingContext();
123+
this.converter = dataAccessStrategy.getConverter();
124+
this.mappingContext = converter.getMappingContext();
120125
this.projectionFactory = new SpelAwareProxyProjectionFactory();
121126
}
122127

@@ -157,6 +162,7 @@ public R2dbcEntityTemplate(DatabaseClient databaseClient, ReactiveDataAccessStra
157162

158163
this.databaseClient = databaseClient;
159164
this.dataAccessStrategy = strategy;
165+
this.converter = dataAccessStrategy.getConverter();
160166
this.mappingContext = strategy.getConverter().getMappingContext();
161167
this.projectionFactory = new SpelAwareProxyProjectionFactory();
162168
}
@@ -173,7 +179,7 @@ public ReactiveDataAccessStrategy getDataAccessStrategy() {
173179

174180
@Override
175181
public R2dbcConverter getConverter() {
176-
return this.dataAccessStrategy.getConverter();
182+
return this.converter;
177183
}
178184

179185
@Override
@@ -334,10 +340,10 @@ <T, P extends Publisher<T>> P doSelect(Query query, Class<?> entityClass, SqlIde
334340
return (P) ((Flux<?>) result).concatMap(it -> maybeCallAfterConvert(it, tableName));
335341
}
336342

337-
private <T> RowsFetchSpec<T> doSelect(Query query, Class<?> entityClass, SqlIdentifier tableName,
343+
private <T> RowsFetchSpec<T> doSelect(Query query, Class<?> entityType, SqlIdentifier tableName,
338344
Class<T> returnType) {
339345

340-
StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass);
346+
StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityType);
341347

342348
StatementMapper.SelectSpec selectSpec = statementMapper //
343349
.createSelect(tableName) //
@@ -362,7 +368,7 @@ private <T> RowsFetchSpec<T> doSelect(Query query, Class<?> entityClass, SqlIden
362368

363369
PreparedOperation<?> operation = statementMapper.getMappedObject(selectSpec);
364370

365-
return getRowsFetchSpec(databaseClient.sql(operation), entityClass, returnType);
371+
return getRowsFetchSpec(databaseClient.sql(operation), entityType, returnType);
366372
}
367373

368374
@Override
@@ -783,19 +789,26 @@ private <T> List<Expression> getSelectProjection(Table table, Query query, Class
783789
return query.getColumns().stream().map(table::column).collect(Collectors.toList());
784790
}
785791

786-
private <T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class<?> entityClass,
787-
Class<T> returnType) {
792+
private <T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class<?> entityType,
793+
Class<T> resultType) {
788794

789-
boolean simpleType;
795+
boolean simpleType = getConverter().isSimpleType(resultType);
790796

791797
BiFunction<Row, RowMetadata, T> rowMapper;
792-
if (returnType.isInterface()) {
793-
simpleType = getConverter().isSimpleType(entityClass);
794-
rowMapper = dataAccessStrategy.getRowMapper(entityClass)
795-
.andThen(o -> projectionFactory.createProjection(returnType, o));
798+
799+
if (simpleType) {
800+
rowMapper = dataAccessStrategy.getRowMapper(resultType);
796801
} else {
797-
simpleType = getConverter().isSimpleType(returnType);
798-
rowMapper = dataAccessStrategy.getRowMapper(returnType);
802+
803+
EntityProjection<T, ?> projection = converter.introspectProjection(resultType, entityType);
804+
805+
rowMapper = (row, rowMetadata) -> {
806+
807+
RowDocument document = dataAccessStrategy.toRowDocument(resultType, row, rowMetadata.getColumnMetadatas());
808+
809+
return projection.isProjection() ? converter.project(projection, document)
810+
: converter.read(resultType, document);
811+
};
799812
}
800813

801814
// avoid top-level null values if the read type is a simple one (e.g. SELECT MAX(age) via Integer.class)

Diff for: spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java

+14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.r2dbc.core;
1717

18+
import io.r2dbc.spi.Readable;
19+
import io.r2dbc.spi.ReadableMetadata;
1820
import io.r2dbc.spi.Row;
1921
import io.r2dbc.spi.RowMetadata;
2022

@@ -25,6 +27,7 @@
2527
import org.springframework.data.r2dbc.mapping.OutboundRow;
2628
import org.springframework.data.relational.core.sql.IdentifierProcessing;
2729
import org.springframework.data.relational.core.sql.SqlIdentifier;
30+
import org.springframework.data.relational.domain.RowDocument;
2831
import org.springframework.lang.Nullable;
2932
import org.springframework.r2dbc.core.Parameter;
3033
import org.springframework.r2dbc.core.PreparedOperation;
@@ -82,6 +85,17 @@ public interface ReactiveDataAccessStrategy {
8285
*/
8386
<T> BiFunction<Row, RowMetadata, T> getRowMapper(Class<T> typeToRead);
8487

88+
/**
89+
* Create a flat {@link RowDocument} from a single {@link Readable Row or Stored Procedure output}.
90+
*
91+
* @param type the underlying entity type.
92+
* @param row the row or stored procedure output to retrieve data from.
93+
* @param metadata readable metadata.
94+
* @return the {@link RowDocument} containing the data.
95+
* @since 3.2
96+
*/
97+
RowDocument toRowDocument(Class<?> type, Readable row, Iterable<? extends ReadableMetadata> metadata);
98+
8599
/**
86100
* @param type
87101
* @return the table name for the {@link Class entity type}.

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

+39-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
import org.junit.jupiter.api.BeforeEach;
3030
import org.junit.jupiter.api.Test;
31-
3231
import org.springframework.data.annotation.Id;
3332
import org.springframework.data.r2dbc.dialect.PostgresDialect;
3433
import org.springframework.data.r2dbc.testing.StatementRecorder;
@@ -103,6 +102,30 @@ void shouldSelectAs() {
103102
assertThat(statement.getSql()).isEqualTo("SELECT person.THE_NAME FROM person WHERE person.THE_NAME = $1");
104103
}
105104

105+
@Test // gh-220
106+
void shouldSelectAsWithColumnName() {
107+
108+
MockRowMetadata metadata = MockRowMetadata.builder()
109+
.columnMetadata(MockColumnMetadata.builder().name("id").type(R2dbcType.INTEGER).build())
110+
.columnMetadata(MockColumnMetadata.builder().name("a_different_name").type(R2dbcType.VARCHAR).build()).build();
111+
MockResult result = MockResult.builder().row(MockRow.builder().identified("id", Object.class, "Walter")
112+
.identified("a_different_name", Object.class, "Werner").metadata(metadata).build()).build();
113+
114+
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
115+
116+
entityTemplate.select(Person.class) //
117+
.as(PersonProjectionWithColumnName.class) //
118+
.matching(query(where("name").is("Walter"))) //
119+
.all() //
120+
.as(StepVerifier::create) //
121+
.assertNext(actual -> assertThat(actual.getName()).isEqualTo("Werner")) //
122+
.verifyComplete();
123+
124+
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT"));
125+
126+
assertThat(statement.getSql()).isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1");
127+
}
128+
106129
@Test // gh-220
107130
void shouldSelectFromTable() {
108131

@@ -234,6 +257,21 @@ public void setName(String name) {
234257
}
235258
}
236259

260+
static class PersonProjectionWithColumnName {
261+
262+
@Id String id;
263+
264+
@Column("a_different_name") String name;
265+
266+
public String getName() {
267+
return name;
268+
}
269+
270+
public void setName(String name) {
271+
this.name = name;
272+
}
273+
}
274+
237275
interface PersonProjection {
238276

239277
String getName();

0 commit comments

Comments
 (0)