#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).

Original pull request: #70.
This commit is contained in:
Mark Paluch
2019-03-12 11:26:58 +01:00
committed by Jens Schauder
parent 39936c67fa
commit 6654db34c4
16 changed files with 544 additions and 50 deletions

View File

@@ -17,10 +17,13 @@ package org.springframework.data.r2dbc.dialect;
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<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();

View File

@@ -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;

View File

@@ -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,13 +32,7 @@ public class SettableValue {
private final @Nullable Object value;
private final Class<?> type;
/**
* Create a {@link SettableValue}.
*
* @param value
* @param type
*/
public SettableValue(@Nullable Object value, Class<?> type) {
private SettableValue(@Nullable Object value, Class<?> type) {
Assert.notNull(type, "Type must not be null");
@@ -45,6 +40,42 @@ public class SettableValue {
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));
}
/**
* Creates a new {@link SettableValue} from {@code value} and {@code type}.
*
* @param value can be {@literal null}.
* @param type must not be {@literal null}.
* @return the {@link SettableValue} value for {@code value}.
*/
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");
return new SettableValue(null, type);
}
/**
* Returns the column value. Can be {@literal null}.
*
@@ -74,6 +105,15 @@ public class SettableValue {
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 class SettableValue {
@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();

View File

@@ -0,0 +1,7 @@
/**
* Domain objects for R2DBC.
*/
@NonNullApi
package org.springframework.data.r2dbc.domain;
import org.springframework.lang.NonNullApi;

View File

@@ -2,7 +2,7 @@ package org.springframework.data.r2dbc.function;
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.

View File

@@ -52,10 +52,10 @@ import org.springframework.dao.DataAccessException;
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 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
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);
}
@@ -378,7 +378,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
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);
}
@@ -390,7 +390,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
() -> 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);
}
@@ -400,7 +400,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
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);
}
@@ -842,7 +842,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
() -> 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);
}
@@ -853,7 +853,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
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);
}

View File

@@ -41,12 +41,12 @@ import org.springframework.data.r2dbc.dialect.BindMarker;
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 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
"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());
}

View File

@@ -18,7 +18,7 @@ package org.springframework.data.r2dbc.function;
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 @@ class MapBindParameterSource implements BindParameterSource {
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;
}

View File

@@ -26,9 +26,9 @@ import java.util.function.BiFunction;
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

View File

@@ -25,6 +25,7 @@ import java.lang.reflect.Array;
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.context.MappingContext;
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 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
}
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<Enum>) target, value.toString());
}
return getConversionService().convert(value, target);
}
private <S> S readEntityFrom(Row row, PersistentProperty<?> property) {
String prefix = property.getName() + "_";
@@ -182,33 +212,101 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
public void write(Object source, OutboundRow sink) {
Class<?> userClass = ClassUtils.getUserClass(source);
RelationalPersistentEntity<?> entity = getRequiredPersistentEntity(userClass);
PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(source);
Optional<Class<?>> 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);
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;
}
sink.put(property.getColumnName(), new SettableValue(writeValue, property.getType()));
Object value = accessor.getProperty(property);
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);
}
}
@SuppressWarnings("unchecked")
private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) {
private void writeSimpleInternal(OutboundRow sink, Object value, RelationalPersistentProperty property) {
sink.put(property.getColumnName(), SettableValue.from(getPotentiallyConvertedSimpleWrite(value)));
}
TypeInformation<?> type = property.getTypeInformation();
Object value = propertyAccessor.getProperty(property);
private void writeNullInternal(OutboundRow sink, RelationalPersistentProperty property) {
RelationalPersistentEntity<?> nestedEntity = getMappingContext()
.getPersistentEntity(type.getRequiredActualType().getType());
sink.put(property.getColumnName(),
SettableValue.empty(getPotentiallyConvertedSimpleNullType(property.getType())));
}
private Class<?> getPotentiallyConvertedSimpleNullType(Class<?> type) {
Optional<Class<?>> customTarget = getConversions().getCustomWriteTarget(type);
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<Class<?>> 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) {

View File

@@ -25,6 +25,7 @@ import org.springframework.data.convert.EntityReader;
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;

View File

@@ -30,12 +30,12 @@ import org.reactivestreams.Publisher;
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;