Skip to content

#59 - Consider custom conversion in MappingR2dbcConverter #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-r2dbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<version>1.0.0.gh-59-SNAPSHOT</version>

<name>Spring Data R2DBC</name>
<description>Spring Data module for R2DBC.</description>
Expand Down
86 changes: 85 additions & 1 deletion src/main/asciidoc/reference/mapping.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,47 @@ Public `JavaBean` properties are not used.
Otherwise, the zero-argument constructor is used.
If there is more than one non-zero-argument constructor, an exception will be thrown.

[[mapping-configuration]]
== Mapping Configuration

Unless explicitly configured, an instance of `MappingR2dbcConverter` is created by default when you create a `DatabaseClient`.
You can create your own instance of the `MappingR2dbcConverter`.
By creating your own instance, you can register Spring converters to map specific classes to and from the database.

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:

.@Configuration class to configure R2DBC mapping support
====
[source,java]
----
@Configuration
public class MyAppConfig extends AbstractR2dbcConfiguration {

public ConnectionFactory connectionFactory() {
return ConnectionFactories.get("r2dbc:…");
}

// the following are optional

@Bean
@Override
public R2dbcCustomConversions r2dbcCustomConversions() {

List<Converter<?, ?>> converterList = new ArrayList<Converter<?, ?>>();
converterList.add(new org.springframework.data.r2dbc.test.PersonReadConverter());
converterList.add(new org.springframework.data.r2dbc.test.PersonWriteConverter());
return new R2dbcCustomConversions(getStoreConversions(), converterList);
}
}
----
====

`AbstractR2dbcConfiguration` requires you to implement a method that defines a `ConnectionFactory`.

You can add additional converters to the converter by overriding the `r2dbcCustomConversions` method.

NOTE: `AbstractR2dbcConfiguration` creates a `DatabaseClient` instance and registers it with the container under the name `databaseClient`.

[[mapping-usage]]
== Metadata-based Mapping

Expand All @@ -52,7 +93,6 @@ public class Person {

private String firstName;

@Indexed
private String lastName;
}
----
Expand Down Expand Up @@ -103,3 +143,47 @@ class OrderItem {

----

[[mapping-explicit-converters]]
=== Overriding Mapping with Explicit Converters

When storing and querying your objects, it is convenient to have a `R2dbcConverter` instance handle the mapping of all Java types to `OutboundRow` instances.
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.

To selectively handle the conversion yourself, register one or more one or more `org.springframework.core.convert.converter.Converter` instances with the `R2dbcConverter`.

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.

NOTE: Custom top-level entity conversion requires asymmetric types for conversion. Inbound data is extracted from R2DBC's `Row`.
Outbound data (to be used with `INSERT`/`UPDATE` statements) is represented as `OutboundRow` and later assembled to a statement.

The following example of a Spring Converter implementation converts from a `Row` to a `Person` POJO:

[source,java]
----
@ReadingConverter
public class PersonReadConverter implements Converter<Row, Person> {

public Person convert(Row source) {
Person p = new Person(source.get("id", String.class),source.get("name", String.class));
p.setAge(source.get("age", Integer.class));
return p;
}
}
----

The following example converts from a `Person` to a `OutboundRow`:

[source,java]
----
@WritingConverter
public class PersonWriteConverter implements Converter<Person, OutboundRow> {

public OutboundRow convert(Person source) {
OutboundRow row = new OutboundRow();
row.put("_d", source.getId());
row.put("name", source.getFirstName());
row.put("age", source.getAge());
return row;
}
}
----
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import io.r2dbc.spi.ConnectionFactory;

import java.util.Collections;
import java.util.Optional;

import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -149,10 +150,18 @@ public ReactiveDataAccessStrategy reactiveDataAccessStrategy(RelationalMappingCo
*/
@Bean
public R2dbcCustomConversions r2dbcCustomConversions() {
return new R2dbcCustomConversions(getStoreConversions(), Collections.emptyList());
}

/**
* Returns the {@link Dialect}-specific {@link StoreConversions}.
*
* @return the {@link Dialect}-specific {@link StoreConversions}.
*/
protected StoreConversions getStoreConversions() {

Dialect dialect = getDialect(connectionFactory());
StoreConversions storeConversions = StoreConversions.of(dialect.getSimpleTypeHolder());
return new R2dbcCustomConversions(storeConversions, R2dbcCustomConversions.STORE_CONVERTERS);
return StoreConversions.of(dialect.getSimpleTypeHolder(), R2dbcCustomConversions.STORE_CONVERTERS);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@

import io.r2dbc.spi.Row;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.r2dbc.domain.OutboundRow;

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

public static final SimpleTypeHolder HOLDER = new R2dbcSimpleTypeHolder();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.function.convert;
package org.springframework.data.r2dbc.domain;

import io.r2dbc.spi.Row;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.function.convert;
package org.springframework.data.r2dbc.domain;

import java.util.Objects;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

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

private SettableValue(@Nullable Object value, Class<?> type) {

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

this.value = value;
this.type = type;
}

/**
* Creates a new {@link SettableValue} from {@code value}.
*
* @param value must not be {@literal null}.
* @return the {@link SettableValue} value for {@code value}.
*/
public static SettableValue from(Object value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates an asymmetry I don't like too much. With one argument you have a factory method, with two args you have to use the constructor.

I'd vote for either only factory methods or constructors but not mixing them in the public API.


Assert.notNull(value, "Value must not be null");

return new SettableValue(value, ClassUtils.getUserClass(value));
}

/**
* Create a {@link SettableValue}.
* Creates a new {@link SettableValue} from {@code value} and {@code type}.
*
* @param value
* @param type
* @param value can be {@literal null}.
* @param type must not be {@literal null}.
* @return the {@link SettableValue} value for {@code value}.
*/
public SettableValue(@Nullable Object value, Class<?> type) {
public static SettableValue fromOrEmpty(@Nullable Object value, Class<?> type) {
return value == null ? empty(type) : new SettableValue(value, ClassUtils.getUserClass(value));
}

/**
* Creates a new empty {@link SettableValue} for {@code type}.
*
* @return the empty {@link SettableValue} value for {@code type}.
*/
public static SettableValue empty(Class<?> type) {

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

this.value = value;
this.type = type;
return new SettableValue(null, type);
}

/**
Expand Down Expand Up @@ -74,6 +105,15 @@ public boolean hasValue() {
return value != null;
}

/**
* Returns whether this {@link SettableValue} has a empty.
*
* @return whether this {@link SettableValue} is empty. {@literal true} if {@link #getValue()} is {@literal null}.
*/
public boolean isEmpty() {
return value == null;
}

@Override
public boolean equals(Object o) {
if (this == o)
Expand All @@ -92,8 +132,8 @@ public int hashCode() {
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [value=").append(value);
sb.append("SettableValue");
sb.append("[value=").append(value);
sb.append(", type=").append(type);
sb.append(']');
return sb.toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Domain objects for R2DBC.
*/
@NonNullApi
package org.springframework.data.r2dbc.domain;

import org.springframework.lang.NonNullApi;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import io.r2dbc.spi.Statement;

import org.springframework.data.r2dbc.function.convert.SettableValue;
import org.springframework.data.r2dbc.domain.SettableValue;

/**
* Extension to {@link QueryOperation} for operations that allow parameter substitution by binding parameter values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.UncategorizedR2dbcException;
import org.springframework.data.r2dbc.domain.OutboundRow;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy;
import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper;
import org.springframework.data.r2dbc.function.convert.OutboundRow;
import org.springframework.data.r2dbc.function.convert.SettableValue;
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
import org.springframework.jdbc.core.SqlProvider;
import org.springframework.lang.Nullable;
Expand Down Expand Up @@ -370,15 +370,15 @@ public ExecuteSpecSupport bind(int index, Object value) {
Assert.notNull(value, () -> String.format("Value at index %d must not be null. Use bindNull(…) instead.", index));

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

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

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

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

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

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

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

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

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

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

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

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

return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
import org.springframework.data.r2dbc.dialect.Dialect;
import org.springframework.data.r2dbc.domain.OutboundRow;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.convert.EntityRowMapper;
import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
import org.springframework.data.r2dbc.function.convert.OutboundRow;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions;
import org.springframework.data.r2dbc.function.convert.SettableValue;
import org.springframework.data.r2dbc.support.StatementRenderUtil;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
Expand Down Expand Up @@ -182,7 +182,7 @@ private SettableValue getArrayValue(SettableValue value, RelationalPersistentPro
"Dialect " + dialect.getClass().getName() + " does not support array columns");
}

return new SettableValue(converter.getArrayValue(arrayColumns, property, value.getValue()),
return SettableValue.fromOrEmpty(converter.getArrayValue(arrayColumns, property, value.getValue()),
property.getActualType());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.data.r2dbc.function.convert.SettableValue;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.util.Assert;

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

this.values.put(paramName, new SettableValue(value, value.getClass()));
this.values.put(paramName, SettableValue.fromOrEmpty(value, value.getClass()));
return this;
}

Expand Down
Loading