diff --git a/pom.xml b/pom.xml
index f50708c4..1b1472d8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data
spring-data-r2dbc
- 1.0.0.BUILD-SNAPSHOT
+ 1.0.0.gh-59-SNAPSHOT
Spring Data R2DBC
Spring Data module for R2DBC.
diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc
index 138ac9a4..7cd2b17a 100644
--- a/src/main/asciidoc/reference/mapping.adoc
+++ b/src/main/asciidoc/reference/mapping.adoc
@@ -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> converterList = new ArrayList>();
+ 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
@@ -52,7 +93,6 @@ public class Person {
private String firstName;
- @Indexed
private String lastName;
}
----
@@ -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 <> 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 {
+
+ 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 {
+
+ 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;
+ }
+}
+----
diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
index 8d7f8fa3..f6570c6b 100644
--- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
+++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
@@ -17,6 +17,7 @@
import io.r2dbc.spi.ConnectionFactory;
+import java.util.Collections;
import java.util.Optional;
import org.springframework.context.annotation.Bean;
@@ -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);
}
/**
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java
index d32d8156..a02b96e3 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java
@@ -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.
@@ -32,7 +35,8 @@ public class R2dbcSimpleTypeHolder extends SimpleTypeHolder {
/**
* Set of R2DBC simple types.
*/
- public static final Set> R2DBC_SIMPLE_TYPES = Collections.singleton(Row.class);
+ public static final Set> R2DBC_SIMPLE_TYPES = Collections
+ .unmodifiableSet(new HashSet<>(Arrays.asList(OutboundRow.class, Row.class)));
public static final SimpleTypeHolder HOLDER = new R2dbcSimpleTypeHolder();
diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java b/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java
similarity index 98%
rename from src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java
rename to src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java
index 1ea2cc5e..c73127fc 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java
+++ b/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java
@@ -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;
diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java b/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java
similarity index 59%
rename from src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java
rename to src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java
index edce1794..6c322464 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java
+++ b/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java
@@ -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.
@@ -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) {
+
+ 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);
}
/**
@@ -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)
@@ -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();
diff --git a/src/main/java/org/springframework/data/r2dbc/domain/package-info.java b/src/main/java/org/springframework/data/r2dbc/domain/package-info.java
new file mode 100644
index 00000000..2ca5d1d8
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/domain/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Domain objects for R2DBC.
+ */
+@NonNullApi
+package org.springframework.data.r2dbc.domain;
+
+import org.springframework.lang.NonNullApi;
diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
index 8038c3b0..941b2cfd 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
@@ -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.
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
index 8da6d444..cb33631f 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
@@ -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;
@@ -370,7 +370,7 @@ 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 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);
}
@@ -378,7 +378,7 @@ public ExecuteSpecSupport bind(int index, Object value) {
public ExecuteSpecSupport bindNull(int index, Class> type) {
Map 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);
}
@@ -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 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);
}
@@ -400,7 +400,7 @@ public ExecuteSpecSupport bindNull(String name, Class> type) {
Assert.hasText(name, "Parameter name must not be null or empty!");
Map 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);
}
@@ -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 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);
}
@@ -853,7 +853,7 @@ public GenericInsertSpec nullValue(String field, Class> type) {
Assert.notNull(field, "Field must not be null!");
Map 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);
}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
index ca482d1f..0793dec9 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
@@ -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;
@@ -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());
}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java
index a6d79f4d..21ca25ec 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java
@@ -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;
/**
@@ -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;
}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
index aff9f794..6c989fec 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
@@ -26,9 +26,9 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
-import org.springframework.data.r2dbc.function.convert.OutboundRow;
+import org.springframework.data.r2dbc.domain.OutboundRow;
+import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
-import org.springframework.data.r2dbc.function.convert.SettableValue;
/**
* Draft of a data access strategy that generalizes convenience operations using mapped entities. Typically used
diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
index f851534f..fb60411d 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
@@ -25,6 +25,7 @@
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.Optional;
import java.util.function.BiFunction;
import org.springframework.core.convert.ConversionService;
@@ -38,6 +39,8 @@
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.r2dbc.dialect.ArrayColumns;
+import org.springframework.data.r2dbc.domain.OutboundRow;
+import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@@ -135,13 +138,40 @@ private Object readFrom(Row row, RelationalPersistentProperty property, String p
}
Object value = row.get(prefix + property.getColumnName());
- return readValue(value, property.getTypeInformation());
+ return getPotentiallyConvertedSimpleRead(value, property.getTypeInformation().getType());
} catch (Exception o_O) {
throw new MappingException(String.format("Could not read property %s from result set!", property), o_O);
}
}
+ /**
+ * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies
+ * {@link Enum} handling or returns the value as is.
+ *
+ * @param value
+ * @param target must not be {@literal null}.
+ * @return
+ */
+ @Nullable
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class> target) {
+
+ if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) {
+ return value;
+ }
+
+ if (getConversions().hasCustomReadTarget(value.getClass(), target)) {
+ return getConversionService().convert(value, target);
+ }
+
+ if (Enum.class.isAssignableFrom(target)) {
+ return Enum.valueOf((Class) target, value.toString());
+ }
+
+ return getConversionService().convert(value, target);
+ }
+
private S readEntityFrom(Row row, PersistentProperty> property) {
String prefix = property.getName() + "_";
@@ -182,33 +212,101 @@ private S createInstance(Row row, String prefix, RelationalPersistentEntity<
public void write(Object source, OutboundRow sink) {
Class> userClass = ClassUtils.getUserClass(source);
+
+ Optional> customTarget = getConversions().getCustomWriteTarget(userClass, OutboundRow.class);
+ if (customTarget.isPresent()) {
+
+ OutboundRow result = getConversionService().convert(source, OutboundRow.class);
+ sink.putAll(result);
+ return;
+ }
+
+ writeInternal(source, sink, userClass);
+ }
+
+ private void writeInternal(Object source, OutboundRow sink, Class> userClass) {
+
RelationalPersistentEntity> entity = getRequiredPersistentEntity(userClass);
+ PersistentPropertyAccessor> propertyAccessor = entity.getPropertyAccessor(source);
- PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(source);
+ writeProperties(sink, entity, propertyAccessor);
+ }
+
+ private void writeProperties(OutboundRow sink, RelationalPersistentEntity> entity,
+ PersistentPropertyAccessor> accessor) {
for (RelationalPersistentProperty property : entity) {
- Object writeValue = getWriteValue(propertyAccessor, property);
+ if (!property.isWritable()) {
+ continue;
+ }
+
+ Object value = accessor.getProperty(property);
- sink.put(property.getColumnName(), new SettableValue(writeValue, property.getType()));
+ if (value == null) {
+ writeNullInternal(sink, property);
+ continue;
+ }
+
+ if (!getConversions().isSimpleType(value.getClass())) {
+
+ RelationalPersistentEntity> nestedEntity = getMappingContext().getPersistentEntity(property.getActualType());
+ if (nestedEntity != null) {
+ throw new InvalidDataAccessApiUsageException("Nested entities are not supported");
+ }
+ }
+
+ writeSimpleInternal(sink, value, property);
}
+ }
+ private void writeSimpleInternal(OutboundRow sink, Object value, RelationalPersistentProperty property) {
+ sink.put(property.getColumnName(), SettableValue.from(getPotentiallyConvertedSimpleWrite(value)));
}
- @SuppressWarnings("unchecked")
- private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) {
+ private void writeNullInternal(OutboundRow sink, RelationalPersistentProperty property) {
- TypeInformation> type = property.getTypeInformation();
- Object value = propertyAccessor.getProperty(property);
+ sink.put(property.getColumnName(),
+ SettableValue.empty(getPotentiallyConvertedSimpleNullType(property.getType())));
+ }
+
+ private Class> getPotentiallyConvertedSimpleNullType(Class> type) {
+
+ Optional> customTarget = getConversions().getCustomWriteTarget(type);
- RelationalPersistentEntity> nestedEntity = getMappingContext()
- .getPersistentEntity(type.getRequiredActualType().getType());
+ if (customTarget.isPresent()) {
+ return customTarget.get();
- if (nestedEntity != null) {
- throw new InvalidDataAccessApiUsageException("Nested entities are not supported");
}
- return value;
+ if (type.isEnum()) {
+ return String.class;
+ }
+
+ return type;
+ }
+
+ /**
+ * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type.
+ * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is.
+ *
+ * @param value
+ * @return
+ */
+ @Nullable
+ private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value) {
+
+ if (value == null) {
+ return null;
+ }
+
+ Optional> customTarget = getConversions().getCustomWriteTarget(value.getClass());
+
+ if (customTarget.isPresent()) {
+ return getConversionService().convert(value, customTarget.get());
+ }
+
+ return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum>) value).name() : value;
}
public Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value) {
diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java
index 6cf89316..f205913f 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java
@@ -25,6 +25,7 @@
import org.springframework.data.convert.EntityWriter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.dialect.ArrayColumns;
+import org.springframework.data.r2dbc.domain.OutboundRow;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
index 4b28127c..c985a573 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
@@ -30,12 +30,12 @@
import org.springframework.data.r2dbc.dialect.BindMarker;
import org.springframework.data.r2dbc.dialect.BindMarkers;
+import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.BindIdOperation;
import org.springframework.data.r2dbc.function.DatabaseClient;
import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec;
import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
-import org.springframework.data.r2dbc.function.convert.SettableValue;
import org.springframework.data.relational.core.sql.Conditions;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Functions;
diff --git a/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java b/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java
new file mode 100644
index 00000000..166deb7f
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.domain;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link SettableValue}.
+ *
+ * @author Mark Paluch
+ */
+public class SettableValueUnitTests {
+
+ @Test // gh-59
+ public void shouldCreateSettableValue() {
+
+ SettableValue value = SettableValue.from("foo");
+
+ assertThat(value.isEmpty()).isFalse();
+ assertThat(value.hasValue()).isTrue();
+ assertThat(value).isEqualTo(SettableValue.from("foo"));
+ }
+
+ @Test // gh-59
+ public void shouldCreateEmpty() {
+
+ SettableValue value = SettableValue.empty(Object.class);
+
+ assertThat(value.isEmpty()).isTrue();
+ assertThat(value.hasValue()).isFalse();
+ assertThat(value).isEqualTo(SettableValue.empty(Object.class));
+ assertThat(value).isNotEqualTo(SettableValue.empty(String.class));
+ }
+
+ @Test // gh-59
+ public void shouldCreatePotentiallyEmpty() {
+
+ assertThat(SettableValue.fromOrEmpty("foo", Object.class).isEmpty()).isFalse();
+ assertThat(SettableValue.fromOrEmpty(null, Object.class).isEmpty()).isTrue();
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
index 59690d5b..2d6c2211 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
@@ -14,7 +14,7 @@
import org.junit.Test;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
-import org.springframework.data.r2dbc.function.convert.SettableValue;
+import org.springframework.data.r2dbc.domain.SettableValue;
/**
* Unit tests for {@link DefaultReactiveDataAccessStrategy}.
diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java
index 9fcbe4c0..1a292f52 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java
@@ -21,9 +21,20 @@
import io.r2dbc.spi.Row;
import lombok.AllArgsConstructor;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.Before;
import org.junit.Test;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
+import org.springframework.data.convert.ReadingConverter;
+import org.springframework.data.convert.WritingConverter;
+import org.springframework.data.r2dbc.domain.OutboundRow;
+import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
/**
@@ -33,7 +44,21 @@
*/
public class MappingR2dbcConverterUnitTests {
- MappingR2dbcConverter converter = new MappingR2dbcConverter(new RelationalMappingContext());
+ RelationalMappingContext mappingContext = new RelationalMappingContext();
+ MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext);
+
+ @Before
+ public void before() {
+
+ R2dbcCustomConversions conversions = new R2dbcCustomConversions(
+ Arrays.asList(StringToMapConverter.INSTANCE, MapToStringConverter.INSTANCE,
+ CustomConversionPersonToOutboundRowConverter.INSTANCE, RowToCustomConversionPerson.INSTANCE));
+
+ mappingContext = new RelationalMappingContext();
+ mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
+
+ converter = new MappingR2dbcConverter(mappingContext, conversions);
+ }
@Test // gh-61
public void shouldIncludeAllPropertiesInOutboundRow() {
@@ -42,9 +67,9 @@ public void shouldIncludeAllPropertiesInOutboundRow() {
converter.write(new Person("id", "Walter", "White"), row);
- assertThat(row).containsEntry("id", new SettableValue("id", String.class));
- assertThat(row).containsEntry("firstname", new SettableValue("Walter", String.class));
- assertThat(row).containsEntry("lastname", new SettableValue("White", String.class));
+ assertThat(row).containsEntry("id", SettableValue.fromOrEmpty("id", String.class));
+ assertThat(row).containsEntry("firstname", SettableValue.fromOrEmpty("Walter", String.class));
+ assertThat(row).containsEntry("lastname", SettableValue.fromOrEmpty("White", String.class));
}
@Test // gh-41
@@ -68,9 +93,188 @@ public void shouldConvertRowToNumber() {
assertThat(result).isEqualTo(42);
}
+ @Test // gh-59
+ public void shouldFailOnUnsupportedEntity() {
+
+ PersonWithConversions withMap = new PersonWithConversions(null, null, new NonMappableEntity());
+ OutboundRow row = new OutboundRow();
+
+ assertThatThrownBy(() -> converter.write(withMap, row)).isInstanceOf(InvalidDataAccessApiUsageException.class);
+ }
+
+ @Test // gh-59
+ public void shouldConvertMapToString() {
+
+ PersonWithConversions withMap = new PersonWithConversions("foo", Collections.singletonMap("map", "value"), null);
+ OutboundRow row = new OutboundRow();
+ converter.write(withMap, row);
+
+ assertThat(row).containsEntry("nested", SettableValue.from("map"));
+ }
+
+ @Test // gh-59
+ public void shouldReadMapFromString() {
+
+ Row rowMock = mock(Row.class);
+ when(rowMock.get("nested")).thenReturn("map");
+
+ PersonWithConversions result = converter.read(PersonWithConversions.class, rowMock);
+
+ assertThat(result.nested).isEqualTo(Collections.singletonMap("map", "map"));
+ }
+
+ @Test // gh-59
+ public void shouldConvertEnum() {
+
+ WithEnum withMap = new WithEnum("foo", Condition.Mint);
+ OutboundRow row = new OutboundRow();
+ converter.write(withMap, row);
+
+ assertThat(row).containsEntry("condition", SettableValue.from("Mint"));
+ }
+
+ @Test // gh-59
+ public void shouldConvertNullEnum() {
+
+ WithEnum withMap = new WithEnum("foo", null);
+ OutboundRow row = new OutboundRow();
+ converter.write(withMap, row);
+
+ assertThat(row).containsEntry("condition", SettableValue.fromOrEmpty(null, String.class));
+ }
+
+ @Test // gh-59
+ public void shouldReadEnum() {
+
+ Row rowMock = mock(Row.class);
+ when(rowMock.get("condition")).thenReturn("Mint");
+
+ WithEnum result = converter.read(WithEnum.class, rowMock);
+
+ assertThat(result.condition).isEqualTo(Condition.Mint);
+ }
+
+ @Test // gh-59
+ public void shouldWriteTopLevelEntity() {
+
+ CustomConversionPerson person = new CustomConversionPerson();
+ person.entity = new NonMappableEntity();
+ person.foo = "bar";
+
+ OutboundRow row = new OutboundRow();
+ converter.write(person, row);
+
+ assertThat(row).containsEntry("foo_column", SettableValue.from("bar")).containsEntry("entity",
+ SettableValue.from("nested_entity"));
+ }
+
+ @Test // gh-59
+ public void shouldReadTopLevelEntity() {
+
+ Row rowMock = mock(Row.class);
+ when(rowMock.get("foo_column", String.class)).thenReturn("bar");
+ when(rowMock.get("nested_entity")).thenReturn("map");
+
+ CustomConversionPerson result = converter.read(CustomConversionPerson.class, rowMock);
+
+ assertThat(result.foo).isEqualTo("bar");
+ assertThat(result.entity).isNotNull();
+ }
+
@AllArgsConstructor
static class Person {
@Id String id;
String firstname, lastname;
}
+
+ @AllArgsConstructor
+ static class WithEnum {
+ @Id String id;
+ Condition condition;
+ }
+
+ enum Condition {
+ Mint, Used
+ }
+
+ @AllArgsConstructor
+ static class PersonWithConversions {
+ @Id String id;
+ Map nested;
+ NonMappableEntity unsupported;
+ }
+
+ static class CustomConversionPerson {
+
+ String foo;
+ NonMappableEntity entity;
+ }
+
+ static class NonMappableEntity {}
+
+ @ReadingConverter
+ enum StringToMapConverter implements Converter> {
+
+ INSTANCE;
+
+ @Override
+ public Map convert(String source) {
+
+ if (source != null) {
+ return Collections.singletonMap(source, source);
+ }
+
+ return null;
+ }
+ }
+
+ @WritingConverter
+ enum MapToStringConverter implements Converter