#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:
committed by
Jens Schauder
parent
39936c67fa
commit
6654db34c4
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Domain objects for R2DBC.
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.springframework.data.r2dbc.domain;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user