Skip to content

Commit 851cbaf

Browse files
committed
#260 - Support interface projections with DatabaseClient.as(…).
We now support interface projections when using as(Class) through ProjectionFactory. Simple, type-less queries (execute, select from table) are backed by Map implementations and require the projection type to return a similar type than the expected value. Simple types (such as numeric types) are converted between the backing result and the projection. Conversion of complex types requires a source with type information such as a typed select (select().from(Person.class).as(PersonProjection.class)) to apply registered converters on property-level.
1 parent d618985 commit 851cbaf

File tree

6 files changed

+224
-16
lines changed

6 files changed

+224
-16
lines changed

Diff for: src/main/asciidoc/reference/r2dbc-fluent.adoc

+6-2
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,18 @@ Mono<Person> first = databaseClient.select()
4040
<1> Selecting from a table by name returns row results as `Map<String, Object>` with case-insensitive column name matching.
4141
<2> The issued query declares a `WHERE` condition on `firstname` and `lastname` columns to filter results.
4242
<3> Results can be ordered by individual column names, resulting in an `ORDER BY` clause.
43-
<4> Selecting the one result fetches only a single row. This way of consuming rows expects the query to return exactly a single result.
43+
<4> Selecting the one result fetches only a single row.
44+
This way of consuming rows expects the query to return exactly a single result.
4445
`Mono` emits a `IncorrectResultSizeDataAccessException` if the query yields more than a single result.
4546
====
4647

48+
TIP: You can directly apply <<projections,Projections>> to result documents by providing the target type via `as(Class<?>)`.
49+
4750
You can consume Query results in three ways:
4851

4952
* Through object mapping (for example, `as(Class<T>)`) by using Spring Data's mapping-metadata.
50-
* As `Map<String, Object>` where column names are mapped to their value. Column names are looked up in a case-insensitive way.
53+
* As `Map<String, Object>` where column names are mapped to their value.
54+
Column names are looked up in a case-insensitive way.
5155
* By supplying a mapping `BiFunction` for direct access to R2DBC `Row` and `RowMetadata`.
5256

5357
You can switch between retrieving a single entity and retrieving multiple entities through the following terminating methods:

Diff for: src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.core.convert.converter.Converter;
3131
import org.springframework.data.convert.CustomConversions;
3232
import org.springframework.data.convert.CustomConversions.StoreConversions;
33+
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
3334
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
3435
import org.springframework.data.r2dbc.convert.R2dbcCustomConversions;
3536
import org.springframework.data.r2dbc.core.DatabaseClient;
@@ -106,10 +107,17 @@ public DatabaseClient databaseClient(ReactiveDataAccessStrategy dataAccessStrate
106107
Assert.notNull(dataAccessStrategy, "DataAccessStrategy must not be null!");
107108
Assert.notNull(exceptionTranslator, "ExceptionTranslator must not be null!");
108109

110+
SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
111+
if (context != null) {
112+
projectionFactory.setBeanFactory(context);
113+
projectionFactory.setBeanClassLoader(context.getClassLoader());
114+
}
115+
109116
return DatabaseClient.builder() //
110117
.connectionFactory(lookupConnectionFactory()) //
111118
.dataAccessStrategy(dataAccessStrategy) //
112119
.exceptionTranslator(exceptionTranslator) //
120+
.projectionFactory(projectionFactory) //
113121
.build();
114122
}
115123

Diff for: src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java

+10
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import org.springframework.data.domain.Pageable;
3232
import org.springframework.data.domain.Sort;
33+
import org.springframework.data.projection.ProjectionFactory;
3334
import org.springframework.data.r2dbc.mapping.SettableValue;
3435
import org.springframework.data.r2dbc.query.Criteria;
3536
import org.springframework.data.r2dbc.query.Update;
@@ -157,6 +158,15 @@ interface Builder {
157158
*/
158159
Builder namedParameters(boolean enabled);
159160

161+
/**
162+
* Configures the {@link org.springframework.data.projection.ProjectionFactory projection factory}.
163+
*
164+
* @param factory must not be {@literal null}.
165+
* @return {@code this} {@link Builder}.
166+
* @since 1.1
167+
*/
168+
Builder projectionFactory(ProjectionFactory factory);
169+
160170
/**
161171
* Configures a {@link Consumer} to configure this builder.
162172
*

Diff for: src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java

+54-7
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.dao.InvalidDataAccessApiUsageException;
5050
import org.springframework.data.domain.Pageable;
5151
import org.springframework.data.domain.Sort;
52+
import org.springframework.data.projection.ProjectionFactory;
5253
import org.springframework.data.r2dbc.UncategorizedR2dbcException;
5354
import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils;
5455
import org.springframework.data.r2dbc.connectionfactory.ConnectionProxy;
@@ -82,13 +83,17 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
8283

8384
private final DefaultDatabaseClientBuilder builder;
8485

86+
private final ProjectionFactory projectionFactory;
87+
8588
DefaultDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
86-
ReactiveDataAccessStrategy dataAccessStrategy, boolean namedParameters, DefaultDatabaseClientBuilder builder) {
89+
ReactiveDataAccessStrategy dataAccessStrategy, boolean namedParameters, ProjectionFactory projectionFactory,
90+
DefaultDatabaseClientBuilder builder) {
8791

8892
this.connector = connector;
8993
this.exceptionTranslator = exceptionTranslator;
9094
this.dataAccessStrategy = dataAccessStrategy;
9195
this.namedParameters = namedParameters;
96+
this.projectionFactory = projectionFactory;
9297
this.builder = builder;
9398
}
9499

@@ -544,7 +549,7 @@ protected ExecuteSpecSupport createInstance(Map<Integer, SettableValue> byIndex,
544549
@SuppressWarnings("unchecked")
545550
protected class DefaultTypedExecuteSpec<T> extends ExecuteSpecSupport implements TypedExecuteSpec<T> {
546551

547-
private final Class<T> typeToRead;
552+
private final @Nullable Class<T> typeToRead;
548553
private final BiFunction<Row, RowMetadata, T> mappingFunction;
549554

550555
DefaultTypedExecuteSpec(Map<Integer, SettableValue> byIndex, Map<String, SettableValue> byName,
@@ -553,7 +558,13 @@ protected class DefaultTypedExecuteSpec<T> extends ExecuteSpecSupport implements
553558
super(byIndex, byName, sqlSupplier);
554559

555560
this.typeToRead = typeToRead;
556-
this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead);
561+
562+
if (typeToRead.isInterface()) {
563+
this.mappingFunction = ColumnMapRowMapper.INSTANCE
564+
.andThen(map -> projectionFactory.createProjection(typeToRead, map));
565+
} else {
566+
this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead);
567+
}
557568
}
558569

559570
DefaultTypedExecuteSpec(Map<Integer, SettableValue> byIndex, Map<String, SettableValue> byName,
@@ -638,6 +649,9 @@ public GenericSelectSpec from(String table) {
638649

639650
@Override
640651
public <T> TypedSelectSpec<T> from(Class<T> table) {
652+
653+
assertRegularClass(table);
654+
641655
return new DefaultTypedSelectSpec<>(table);
642656
}
643657
}
@@ -735,8 +749,16 @@ public <R> TypedSelectSpec<R> as(Class<R> resultType) {
735749

736750
Assert.notNull(resultType, "Result type must not be null!");
737751

752+
BiFunction<Row, RowMetadata, R> rowMapper;
753+
754+
if (resultType.isInterface()) {
755+
rowMapper = ColumnMapRowMapper.INSTANCE.andThen(map -> projectionFactory.createProjection(resultType, map));
756+
} else {
757+
rowMapper = dataAccessStrategy.getRowMapper(resultType);
758+
}
759+
738760
return new DefaultTypedSelectSpec<>(this.table, this.projectedFields, this.criteria, this.sort, this.page,
739-
resultType, dataAccessStrategy.getRowMapper(resultType));
761+
resultType, rowMapper);
740762
}
741763

742764
@Override
@@ -808,10 +830,10 @@ protected DefaultGenericSelectSpec createInstance(String table, List<String> pro
808830
@SuppressWarnings("unchecked")
809831
private class DefaultTypedSelectSpec<T> extends DefaultSelectSpecSupport implements TypedSelectSpec<T> {
810832

811-
private final @Nullable Class<T> typeToRead;
833+
private final Class<T> typeToRead;
812834
private final BiFunction<Row, RowMetadata, T> mappingFunction;
813835

814-
DefaultTypedSelectSpec(@Nullable Class<T> typeToRead) {
836+
DefaultTypedSelectSpec(Class<T> typeToRead) {
815837

816838
super(dataAccessStrategy.getTableName(typeToRead));
817839

@@ -833,7 +855,16 @@ public <R> FetchSpec<R> as(Class<R> resultType) {
833855

834856
Assert.notNull(resultType, "Result type must not be null!");
835857

836-
return exchange(dataAccessStrategy.getRowMapper(resultType));
858+
BiFunction<Row, RowMetadata, R> rowMapper;
859+
860+
if (resultType.isInterface()) {
861+
rowMapper = dataAccessStrategy.getRowMapper(typeToRead)
862+
.andThen(r -> projectionFactory.createProjection(resultType, r));
863+
} else {
864+
rowMapper = dataAccessStrategy.getRowMapper(resultType);
865+
}
866+
867+
return exchange(rowMapper);
837868
}
838869

839870
@Override
@@ -920,6 +951,9 @@ public GenericInsertSpec<Map<String, Object>> into(String table) {
920951

921952
@Override
922953
public <T> TypedInsertSpec<T> into(Class<T> table) {
954+
955+
assertRegularClass(table);
956+
923957
return new DefaultTypedInsertSpec<>(table, ColumnMapRowMapper.INSTANCE);
924958
}
925959
}
@@ -1136,6 +1170,9 @@ public GenericUpdateSpec table(String table) {
11361170

11371171
@Override
11381172
public <T> TypedUpdateSpec<T> table(Class<T> table) {
1173+
1174+
assertRegularClass(table);
1175+
11391176
return new DefaultTypedUpdateSpec<>(table, null, null);
11401177
}
11411178
}
@@ -1297,6 +1334,9 @@ public DefaultDeleteSpec<?> from(String table) {
12971334

12981335
@Override
12991336
public <T> DefaultDeleteSpec<T> from(Class<T> table) {
1337+
1338+
assertRegularClass(table);
1339+
13001340
return new DefaultDeleteSpec<>(table, null, null);
13011341
}
13021342
}
@@ -1477,6 +1517,13 @@ private static String getRequiredSql(Supplier<String> sqlSupplier) {
14771517
return sql;
14781518
}
14791519

1520+
private static void assertRegularClass(Class<?> table) {
1521+
1522+
Assert.notNull(table, "Entity type must not be null");
1523+
Assert.isTrue(!table.isInterface() && !table.isEnum(),
1524+
() -> String.format("Entity type %s must be a class", table.getName()));
1525+
}
1526+
14801527
/**
14811528
* Invocation handler that suppresses close calls on R2DBC Connections. Also prepares returned Statement
14821529
* (Prepared/CallbackStatement) objects.

Diff for: src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientBuilder.java

+19-7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.util.function.Consumer;
2222

23+
import org.springframework.data.projection.ProjectionFactory;
2324
import org.springframework.data.r2dbc.core.DatabaseClient.Builder;
2425
import org.springframework.data.r2dbc.dialect.DialectResolver;
2526
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
@@ -43,6 +44,8 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
4344

4445
private boolean namedParameters = true;
4546

47+
private ProjectionFactory projectionFactory;
48+
4649
DefaultDatabaseClientBuilder() {}
4750

4851
DefaultDatabaseClientBuilder(DefaultDatabaseClientBuilder other) {
@@ -53,6 +56,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
5356
this.exceptionTranslator = other.exceptionTranslator;
5457
this.accessStrategy = other.accessStrategy;
5558
this.namedParameters = other.namedParameters;
59+
this.projectionFactory = other.projectionFactory;
5660
}
5761

5862
/*
@@ -105,6 +109,19 @@ public Builder namedParameters(boolean enabled) {
105109
return this;
106110
}
107111

112+
/*
113+
* (non-Javadoc)
114+
* @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#projectionFactory(ProjectionFactory)
115+
*/
116+
@Override
117+
public Builder projectionFactory(ProjectionFactory factory) {
118+
119+
Assert.notNull(factory, "ProjectionFactory must not be null!");
120+
121+
this.projectionFactory = factory;
122+
return this;
123+
}
124+
108125
/*
109126
* (non-Javadoc)
110127
* @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#build()
@@ -126,13 +143,8 @@ public DatabaseClient build() {
126143
accessStrategy = new DefaultReactiveDataAccessStrategy(dialect);
127144
}
128145

129-
return doBuild(this.connectionFactory, exceptionTranslator, accessStrategy, namedParameters,
130-
new DefaultDatabaseClientBuilder(this));
131-
}
132-
133-
protected DatabaseClient doBuild(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
134-
ReactiveDataAccessStrategy accessStrategy, boolean namedParameters, DefaultDatabaseClientBuilder builder) {
135-
return new DefaultDatabaseClient(connector, exceptionTranslator, accessStrategy, namedParameters, builder);
146+
return new DefaultDatabaseClient(this.connectionFactory, exceptionTranslator, accessStrategy, namedParameters,
147+
projectionFactory, new DefaultDatabaseClientBuilder(this));
136148
}
137149

138150
/*

0 commit comments

Comments
 (0)