Skip to content

Commit b68db1b

Browse files
committed
#59 - Consider custom conversion in MappingR2dbcConverter.
MappingR2dbcConverter now considers custom conversions for inbound and outbound conversion of top-level types (Row to Entity, Entity to OutboundRow) and on property level (e.g. convert an object to String and vice versa).
1 parent 75a5c27 commit b68db1b

16 files changed

+541
-47
lines changed

src/main/asciidoc/reference/mapping.adoc

+85-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,47 @@ Public `JavaBean` properties are not used.
2828
Otherwise, the zero-argument constructor is used.
2929
If there is more than one non-zero-argument constructor, an exception will be thrown.
3030

31+
[[mapping-configuration]]
32+
== Mapping Configuration
33+
34+
Unless explicitly configured, an instance of `MappingR2dbcConverter` is created by default when you create a `DatabaseClient`.
35+
You can create your own instance of the `MappingR2dbcConverter`.
36+
By creating your own instance, you can register Spring converters to map specific classes to and from the database.
37+
38+
You can configure the `MappingR2dbcConverter` as well as `DatabaseClient` and `ConnectionFactory` by using Java-based metadata. The following example uses Spring's Java-based configuration:
39+
40+
.@Configuration class to configure R2DBC mapping support
41+
====
42+
[source,java]
43+
----
44+
@Configuration
45+
public class MyAppConfig extends AbstractR2dbcConfiguration {
46+
47+
public ConnectionFactory connectionFactory() {
48+
return ConnectionFactories.get("r2dbc:…");
49+
}
50+
51+
// the following are optional
52+
53+
@Bean
54+
@Override
55+
public R2dbcCustomConversions r2dbcCustomConversions() {
56+
57+
List<Converter<?, ?>> converterList = new ArrayList<Converter<?, ?>>();
58+
converterList.add(new org.springframework.data.r2dbc.test.PersonReadConverter());
59+
converterList.add(new org.springframework.data.r2dbc.test.PersonWriteConverter());
60+
return new R2dbcCustomConversions(getStoreConversions(), converterList);
61+
}
62+
}
63+
----
64+
====
65+
66+
`AbstractR2dbcConfiguration` requires you to implement a method that defines a `ConnectionFactory`.
67+
68+
You can add additional converters to the converter by overriding the `r2dbcCustomConversions` method.
69+
70+
NOTE: `AbstractR2dbcConfiguration` creates a `DatabaseClient` instance and registers it with the container under the name `databaseClient`.
71+
3172
[[mapping-usage]]
3273
== Metadata-based Mapping
3374

@@ -52,7 +93,6 @@ public class Person {
5293
5394
private String firstName;
5495
55-
@Indexed
5696
private String lastName;
5797
}
5898
----
@@ -103,3 +143,47 @@ class OrderItem {
103143
104144
----
105145

146+
[[mapping-explicit-converters]]
147+
=== Overriding Mapping with Explicit Converters
148+
149+
When storing and querying your objects, it is convenient to have a `R2dbcConverter` instance handle the mapping of all Java types to `OutboundRow` instances.
150+
However, sometimes you may want the `R2dbcConverter` instances do most of the work but let you selectively handle the conversion for a particular type -- perhaps to optimize performance.
151+
152+
To selectively handle the conversion yourself, register one or more one or more `org.springframework.core.convert.converter.Converter` instances with the `R2dbcConverter`.
153+
154+
You can use the `r2dbcCustomConversions` method in `AbstractR2dbcConfiguration` to configure converters. The examples <<mapping-configuration, at the beginning of this chapter>> show how to perform the configuration using Java.
155+
156+
NOTE: Custom top-level entity conversion requires asymmetric types for conversion. Inbound data is extracted from R2DBC's `Row`.
157+
Outbound data (to be used with `INSERT`/`UPDATE` statements) is represented as `OutboundRow` and later assembled to a statement.
158+
159+
The following example of a Spring Converter implementation converts from a `Row` to a `Person` POJO:
160+
161+
[source,java]
162+
----
163+
@ReadingConverter
164+
public class PersonReadConverter implements Converter<Row, Person> {
165+
166+
public Person convert(Row source) {
167+
Person p = new Person(source.get("id", String.class),source.get("name", String.class));
168+
p.setAge(source.get("age", Integer.class));
169+
return p;
170+
}
171+
}
172+
----
173+
174+
The following example converts from a `Person` to a `OutboundRow`:
175+
176+
[source,java]
177+
----
178+
@WritingConverter
179+
public class PersonWriteConverter implements Converter<Person, OutboundRow> {
180+
181+
public OutboundRow convert(Person source) {
182+
OutboundRow row = new OutboundRow();
183+
row.put("_d", source.getId());
184+
row.put("name", source.getFirstName());
185+
row.put("age", source.getAge());
186+
return row;
187+
}
188+
}
189+
----

src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717

1818
import io.r2dbc.spi.Row;
1919

20+
import java.util.Arrays;
2021
import java.util.Collections;
22+
import java.util.HashSet;
2123
import java.util.Set;
2224

2325
import org.springframework.data.mapping.model.SimpleTypeHolder;
26+
import org.springframework.data.r2dbc.domain.OutboundRow;
2427

2528
/**
2629
* Simple constant holder for a {@link SimpleTypeHolder} enriched with R2DBC specific simple types.
@@ -32,7 +35,8 @@ public class R2dbcSimpleTypeHolder extends SimpleTypeHolder {
3235
/**
3336
* Set of R2DBC simple types.
3437
*/
35-
public static final Set<Class<?>> R2DBC_SIMPLE_TYPES = Collections.singleton(Row.class);
38+
public static final Set<Class<?>> R2DBC_SIMPLE_TYPES = Collections
39+
.unmodifiableSet(new HashSet<>(Arrays.asList(OutboundRow.class, Row.class)));
3640

3741
public static final SimpleTypeHolder HOLDER = new R2dbcSimpleTypeHolder();
3842

src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java renamed to src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.data.r2dbc.function.convert;
16+
package org.springframework.data.r2dbc.domain;
1717

1818
import io.r2dbc.spi.Row;
1919

src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java renamed to src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java

+49-9
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.data.r2dbc.function.convert;
16+
package org.springframework.data.r2dbc.domain;
1717

1818
import java.util.Objects;
1919

2020
import org.springframework.lang.Nullable;
2121
import org.springframework.util.Assert;
22+
import org.springframework.util.ClassUtils;
2223

2324
/**
2425
* A database value that can be set in a statement.
@@ -31,18 +32,48 @@ public class SettableValue {
3132
private final @Nullable Object value;
3233
private final Class<?> type;
3334

35+
private SettableValue(@Nullable Object value, Class<?> type) {
36+
37+
Assert.notNull(type, "Type must not be null");
38+
39+
this.value = value;
40+
this.type = type;
41+
}
42+
43+
/**
44+
* Creates a new {@link SettableValue} from {@code value}.
45+
*
46+
* @param value must not be {@literal null}.
47+
* @return the {@link SettableValue} value for {@code value}.
48+
*/
49+
public static SettableValue from(Object value) {
50+
51+
Assert.notNull(value, "Value must not be null");
52+
53+
return new SettableValue(value, ClassUtils.getUserClass(value));
54+
}
55+
3456
/**
35-
* Create a {@link SettableValue}.
57+
* Creates a new {@link SettableValue} from {@code value} and {@code type}.
3658
*
37-
* @param value
38-
* @param type
59+
* @param value can be {@literal null}.
60+
* @param type must not be {@literal null}.
61+
* @return the {@link SettableValue} value for {@code value}.
3962
*/
40-
public SettableValue(@Nullable Object value, Class<?> type) {
63+
public static SettableValue fromOrEmpty(@Nullable Object value, Class<?> type) {
64+
return value == null ? empty(type) : new SettableValue(value, ClassUtils.getUserClass(value));
65+
}
66+
67+
/**
68+
* Creates a new empty {@link SettableValue} for {@code type}.
69+
*
70+
* @return the empty {@link SettableValue} value for {@code type}.
71+
*/
72+
public static SettableValue empty(Class<?> type) {
4173

4274
Assert.notNull(type, "Type must not be null");
4375

44-
this.value = value;
45-
this.type = type;
76+
return new SettableValue(null, type);
4677
}
4778

4879
/**
@@ -74,6 +105,15 @@ public boolean hasValue() {
74105
return value != null;
75106
}
76107

108+
/**
109+
* Returns whether this {@link SettableValue} has a empty.
110+
*
111+
* @return whether this {@link SettableValue} is empty. {@literal true} if {@link #getValue()} is {@literal null}.
112+
*/
113+
public boolean isEmpty() {
114+
return value == null;
115+
}
116+
77117
@Override
78118
public boolean equals(Object o) {
79119
if (this == o)
@@ -92,8 +132,8 @@ public int hashCode() {
92132
@Override
93133
public String toString() {
94134
final StringBuffer sb = new StringBuffer();
95-
sb.append(getClass().getSimpleName());
96-
sb.append(" [value=").append(value);
135+
sb.append("SettableValue");
136+
sb.append("[value=").append(value);
97137
sb.append(", type=").append(type);
98138
sb.append(']');
99139
return sb.toString();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Domain objects for R2DBC.
3+
*/
4+
@NonNullApi
5+
package org.springframework.data.r2dbc.domain;
6+
7+
import org.springframework.lang.NonNullApi;

src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import io.r2dbc.spi.Statement;
44

5-
import org.springframework.data.r2dbc.function.convert.SettableValue;
5+
import org.springframework.data.r2dbc.domain.SettableValue;
66

77
/**
88
* Extension to {@link QueryOperation} for operations that allow parameter substitution by binding parameter values.

src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java

+8-8
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@
5252
import org.springframework.data.domain.Pageable;
5353
import org.springframework.data.domain.Sort;
5454
import org.springframework.data.r2dbc.UncategorizedR2dbcException;
55+
import org.springframework.data.r2dbc.domain.OutboundRow;
56+
import org.springframework.data.r2dbc.domain.SettableValue;
5557
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy;
5658
import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper;
57-
import org.springframework.data.r2dbc.function.convert.OutboundRow;
58-
import org.springframework.data.r2dbc.function.convert.SettableValue;
5959
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
6060
import org.springframework.jdbc.core.SqlProvider;
6161
import org.springframework.lang.Nullable;
@@ -370,15 +370,15 @@ public ExecuteSpecSupport bind(int index, Object value) {
370370
Assert.notNull(value, () -> String.format("Value at index %d must not be null. Use bindNull(…) instead.", index));
371371

372372
Map<Integer, SettableValue> byIndex = new LinkedHashMap<>(this.byIndex);
373-
byIndex.put(index, new SettableValue(value, value.getClass()));
373+
byIndex.put(index, SettableValue.fromOrEmpty(value, value.getClass()));
374374

375375
return createInstance(byIndex, this.byName, this.sqlSupplier);
376376
}
377377

378378
public ExecuteSpecSupport bindNull(int index, Class<?> type) {
379379

380380
Map<Integer, SettableValue> byIndex = new LinkedHashMap<>(this.byIndex);
381-
byIndex.put(index, new SettableValue(null, type));
381+
byIndex.put(index, SettableValue.empty(type));
382382

383383
return createInstance(byIndex, this.byName, this.sqlSupplier);
384384
}
@@ -390,7 +390,7 @@ public ExecuteSpecSupport bind(String name, Object value) {
390390
() -> String.format("Value for parameter %s must not be null. Use bindNull(…) instead.", name));
391391

392392
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
393-
byName.put(name, new SettableValue(value, value.getClass()));
393+
byName.put(name, SettableValue.fromOrEmpty(value, value.getClass()));
394394

395395
return createInstance(this.byIndex, byName, this.sqlSupplier);
396396
}
@@ -400,7 +400,7 @@ public ExecuteSpecSupport bindNull(String name, Class<?> type) {
400400
Assert.hasText(name, "Parameter name must not be null or empty!");
401401

402402
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
403-
byName.put(name, new SettableValue(null, type));
403+
byName.put(name, SettableValue.empty(type));
404404

405405
return createInstance(this.byIndex, byName, this.sqlSupplier);
406406
}
@@ -842,7 +842,7 @@ public GenericInsertSpec value(String field, Object value) {
842842
() -> String.format("Value for field %s must not be null. Use nullValue(…) instead.", field));
843843

844844
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
845-
byName.put(field, new SettableValue(value, value.getClass()));
845+
byName.put(field, SettableValue.fromOrEmpty(value, value.getClass()));
846846

847847
return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
848848
}
@@ -853,7 +853,7 @@ public GenericInsertSpec nullValue(String field, Class<?> type) {
853853
Assert.notNull(field, "Field must not be null!");
854854

855855
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
856-
byName.put(field, new SettableValue(null, type));
856+
byName.put(field, SettableValue.empty(type));
857857

858858
return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
859859
}

src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@
4141
import org.springframework.data.r2dbc.dialect.BindMarkers;
4242
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
4343
import org.springframework.data.r2dbc.dialect.Dialect;
44+
import org.springframework.data.r2dbc.domain.OutboundRow;
45+
import org.springframework.data.r2dbc.domain.SettableValue;
4446
import org.springframework.data.r2dbc.function.convert.EntityRowMapper;
4547
import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
46-
import org.springframework.data.r2dbc.function.convert.OutboundRow;
4748
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
4849
import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions;
49-
import org.springframework.data.r2dbc.function.convert.SettableValue;
5050
import org.springframework.data.r2dbc.support.StatementRenderUtil;
5151
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
5252
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@@ -182,7 +182,7 @@ private SettableValue getArrayValue(SettableValue value, RelationalPersistentPro
182182
"Dialect " + dialect.getClass().getName() + " does not support array columns");
183183
}
184184

185-
return new SettableValue(converter.getArrayValue(arrayColumns, property, value.getValue()),
185+
return SettableValue.fromOrEmpty(converter.getArrayValue(arrayColumns, property, value.getValue()),
186186
property.getActualType());
187187
}
188188

src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import java.util.LinkedHashMap;
1919
import java.util.Map;
2020

21-
import org.springframework.data.r2dbc.function.convert.SettableValue;
21+
import org.springframework.data.r2dbc.domain.SettableValue;
2222
import org.springframework.util.Assert;
2323

2424
/**
@@ -65,7 +65,7 @@ MapBindParameterSource addValue(String paramName, Object value) {
6565
Assert.notNull(paramName, "Parameter name must not be null!");
6666
Assert.notNull(value, "Value must not be null!");
6767

68-
this.values.put(paramName, new SettableValue(value, value.getClass()));
68+
this.values.put(paramName, SettableValue.fromOrEmpty(value, value.getClass()));
6969
return this;
7070
}
7171

src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
import org.springframework.data.domain.Pageable;
2727
import org.springframework.data.domain.Sort;
2828
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
29-
import org.springframework.data.r2dbc.function.convert.OutboundRow;
29+
import org.springframework.data.r2dbc.domain.OutboundRow;
30+
import org.springframework.data.r2dbc.domain.SettableValue;
3031
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
31-
import org.springframework.data.r2dbc.function.convert.SettableValue;
3232

3333
/**
3434
* Draft of a data access strategy that generalizes convenience operations using mapped entities. Typically used

0 commit comments

Comments
 (0)