Migrate JDBC to use RowDocument for reading aggregates.

Original pull request #1618
See #1554
This commit is contained in:
Mark Paluch
2023-09-19 11:54:05 +02:00
committed by Jens Schauder
parent 665ae6b5a8
commit 4e3120ee0e
11 changed files with 681 additions and 113 deletions

View File

@@ -20,8 +20,10 @@ import java.sql.JDBCType;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLType;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -44,13 +46,17 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.relational.core.conversion.MappingRelationalConverter;
import org.springframework.data.relational.core.conversion.ObjectPath;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.conversion.RowDocumentAccessor;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -83,18 +89,15 @@ public class BasicJdbcConverter extends MappingRelationalConverter implements Jd
private SpELContext spELContext;
/**
* Creates a new {@link BasicJdbcConverter} given {@link MappingContext} and a
* {@link JdbcTypeFactory#unsupported() no-op type factory} throwing {@link UnsupportedOperationException} on type
* creation. Use
* Creates a new {@link BasicJdbcConverter} given {@link MappingContext} and a {@link JdbcTypeFactory#unsupported()
* no-op type factory} throwing {@link UnsupportedOperationException} on type creation. Use
* {@link #BasicJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory, IdentifierProcessing)}
* (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types.
*
* @param context must not be {@literal null}.
* @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
*/
public BasicJdbcConverter(
RelationalMappingContext context,
RelationResolver relationResolver) {
public BasicJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) {
super(context, new JdbcCustomConversions());
@@ -115,10 +118,8 @@ public class BasicJdbcConverter extends MappingRelationalConverter implements Jd
* @param identifierProcessing must not be {@literal null}
* @since 2.0
*/
public BasicJdbcConverter(
RelationalMappingContext context,
RelationResolver relationResolver, CustomConversions conversions, JdbcTypeFactory typeFactory,
IdentifierProcessing identifierProcessing) {
public BasicJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver,
CustomConversions conversions, JdbcTypeFactory typeFactory, IdentifierProcessing identifierProcessing) {
super(context, conversions);
@@ -300,16 +301,241 @@ public class BasicJdbcConverter extends MappingRelationalConverter implements Jd
@Override
public <T> T mapRow(RelationalPersistentEntity<T> entity, ResultSet resultSet, Object key) {
return new ReadingContext<T>(getMappingContext().getAggregatePath( entity),
new ResultSetAccessor(resultSet), Identifier.empty(), key).mapRow();
return new ReadingContext<T>(getMappingContext().getAggregatePath(entity), new ResultSetAccessor(resultSet),
Identifier.empty(), key).mapRow();
}
@Override
public <T> T mapRow(AggregatePath path, ResultSet resultSet, Identifier identifier, Object key) {
return new ReadingContext<T>(path, new ResultSetAccessor(resultSet), identifier, key).mapRow();
}
@Override
public <R> R projectAndResolve(EntityProjection<R, ?> projection, RowDocument document) {
RelationalPersistentEntity<?> entity = getMappingContext()
.getRequiredPersistentEntity(projection.getActualDomainType());
ResolvingConversionContext context = new ResolvingConversionContext(newProjectingConversionContext(projection),
getMappingContext().getAggregatePath(entity), Identifier.empty());
return doReadProjection(context, document, projection);
}
@SuppressWarnings("unchecked")
@Override
public <R> R readAndResolve(Class<R> type, RowDocument source, Identifier identifier) {
RelationalPersistentEntity<R> entity = (RelationalPersistentEntity<R>) getMappingContext()
.getRequiredPersistentEntity(type);
AggregatePath path = getMappingContext().getAggregatePath(entity);
Identifier identifierToUse = ResolvingRelationalPropertyValueProvider.potentiallyAppendIdentifier(identifier,
entity, it -> source.get(it.getColumnName().getReference()));
ResolvingConversionContext context = new ResolvingConversionContext(getConversionContext(ObjectPath.ROOT), path,
identifierToUse);
return readAggregate(context, source, entity.getTypeInformation());
}
@Override
protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor,
SpELExpressionEvaluator evaluator, ConversionContext context) {
if (context instanceof ResolvingConversionContext rcc) {
AggregatePathValueProvider delegate = (AggregatePathValueProvider) super.newValueProvider(documentAccessor,
evaluator, context);
return new ResolvingRelationalPropertyValueProvider(delegate, documentAccessor, rcc, rcc.identifier());
}
return super.newValueProvider(documentAccessor, evaluator, context);
}
/**
* {@link RelationalPropertyValueProvider} using a resolving context to lookup relations. This is highly
* context-sensitive. Note that the identifier is held here because of a chicken and egg problem, while
* {@link ResolvingConversionContext} hols the {@link AggregatePath}.
*/
class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValueProvider {
private final AggregatePathValueProvider delegate;
private final RowDocumentAccessor accessor;
private final ResolvingConversionContext context;
private final Identifier identifier;
private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor,
ResolvingConversionContext context, Identifier identifier) {
AggregatePath path = context.aggregatePath();
this.delegate = delegate;
this.accessor = accessor;
this.context = context;
this.identifier = path.isEntity()
? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(), delegate::getPropertyValue)
: identifier;
}
/**
* Conditionally append the identifier if the entity has an identifier property.
*/
static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity<?> entity,
Function<RelationalPersistentProperty, Object> getter) {
if (entity.hasIdProperty()) {
RelationalPersistentProperty idProperty = entity.getRequiredIdProperty();
Object propertyValue = getter.apply(idProperty);
if (propertyValue != null) {
return base.withPart(idProperty.getColumnName(), propertyValue, idProperty.getType());
}
}
return base;
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public <T> T getPropertyValue(RelationalPersistentProperty property) {
AggregatePath aggregatePath = this.context.aggregatePath();
if (getConversions().isSimpleType(property.getActualType())) {
return (T) delegate.getValue(aggregatePath);
}
if (property.isEntity()) {
if (property.isCollectionLike() || property.isMap()) {
Identifier identifier1 = this.identifier;
if (property.getOwner().hasIdProperty()) {
Object id = this.identifier.get(property.getOwner().getRequiredIdProperty().getColumnName());
if (id != null) {
identifier1 = Identifier.of(aggregatePath.getTableInfo().reverseColumnInfo().name(), id, Object.class);
}
}
Iterable<Object> allByPath = relationResolver.findAllByPath(identifier1,
aggregatePath.getRequiredPersistentPropertyPath());
if (property.isCollectionLike()) {
return (T) allByPath;
}
if (property.isMap()) {
return (T) ITERABLE_OF_ENTRY_TO_MAP_CONVERTER.convert(allByPath);
}
Iterator<Object> iterator = allByPath.iterator();
if (iterator.hasNext()) {
return (T) iterator.next();
}
return null;
}
return hasValue(property) ? (T) readAggregate(this.context, accessor, property.getTypeInformation()) : null;
}
return (T) delegate.getValue(aggregatePath);
}
@Override
public boolean hasValue(RelationalPersistentProperty property) {
if (property.isCollectionLike() || property.isMap()) {
// attempt relation fetch
return true;
}
AggregatePath aggregatePath = context.aggregatePath();
if (property.isEntity()) {
RelationalPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(property);
if (entity.hasIdProperty()) {
RelationalPersistentProperty referenceId = entity.getRequiredIdProperty();
AggregatePath toUse = aggregatePath.append(referenceId);
return delegate.hasValue(toUse);
}
return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias());
}
return delegate.hasValue(aggregatePath);
}
@Override
public RelationalPropertyValueProvider withContext(ConversionContext context) {
return context == this.context ? this
: new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor,
(ResolvingConversionContext) context, identifier);
}
}
/**
* Marker object to indicate that the property value provider should resolve relations.
*
* @param delegate
* @param aggregatePath
* @param identifier
*/
private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath,
Identifier identifier) implements ConversionContext {
@Override
public <S> S convert(Object source, TypeInformation<? extends S> typeHint) {
return delegate.convert(source, typeHint);
}
@Override
public <S> S convert(Object source, TypeInformation<? extends S> typeHint, ConversionContext context) {
return delegate.convert(source, typeHint, context);
}
@Override
public ResolvingConversionContext forProperty(String name) {
RelationalPersistentProperty property = aggregatePath.getRequiredLeafEntity().getRequiredPersistentProperty(name);
return forProperty(property);
}
@Override
public ResolvingConversionContext forProperty(RelationalPersistentProperty property) {
ConversionContext nested = delegate.forProperty(property);
return new ResolvingConversionContext(nested, aggregatePath.append(property), identifier);
}
@Override
public ResolvingConversionContext withPath(ObjectPath currentPath) {
return new ResolvingConversionContext(delegate.withPath(currentPath), aggregatePath, identifier);
}
@Override
public ObjectPath getPath() {
return delegate.getPath();
}
@Override
public CustomConversions getCustomConversions() {
return delegate.getCustomConversions();
}
@Override
public RelationalConverter getSourceConverter() {
return delegate.getSourceConverter();
}
}
static Object[] requireObjectArray(Object source) {
Assert.isTrue(source.getClass().isArray(), "Source object is not an array");
@@ -361,15 +587,14 @@ public class BasicJdbcConverter extends MappingRelationalConverter implements Jd
private final ResultSetAccessor accessor;
@SuppressWarnings("unchecked")
private ReadingContext(AggregatePath rootPath, ResultSetAccessor accessor, Identifier identifier,
Object key) {
private ReadingContext(AggregatePath rootPath, ResultSetAccessor accessor, Identifier identifier, Object key) {
RelationalPersistentEntity<T> entity = (RelationalPersistentEntity<T>) rootPath.getLeafEntity();
Assert.notNull(entity, "The rootPath must point to an entity");
this.entity = entity;
this.rootPath = rootPath;
this.path = getMappingContext().getAggregatePath( this.entity);
this.path = getMappingContext().getAggregatePath(this.entity);
this.identifier = identifier;
this.key = key;
this.propertyValueProvider = new JdbcPropertyValueProvider(path, accessor);
@@ -377,9 +602,8 @@ public class BasicJdbcConverter extends MappingRelationalConverter implements Jd
this.accessor = accessor;
}
private ReadingContext(RelationalPersistentEntity<T> entity, AggregatePath rootPath,
AggregatePath path, Identifier identifier, Object key,
JdbcPropertyValueProvider propertyValueProvider,
private ReadingContext(RelationalPersistentEntity<T> entity, AggregatePath rootPath, AggregatePath path,
Identifier identifier, Object key, JdbcPropertyValueProvider propertyValueProvider,
JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider, ResultSetAccessor accessor) {
this.entity = entity;
@@ -396,8 +620,8 @@ public class BasicJdbcConverter extends MappingRelationalConverter implements Jd
return new ReadingContext<>(
(RelationalPersistentEntity<S>) getMappingContext().getRequiredPersistentEntity(property.getActualType()),
rootPath.append(property), path.append(property), identifier, key,
propertyValueProvider.extendBy(property), backReferencePropertyValueProvider.extendBy(property), accessor);
rootPath.append(property), path.append(property), identifier, key, propertyValueProvider.extendBy(property),
backReferencePropertyValueProvider.extendBy(property), accessor);
}
T mapRow() {

View File

@@ -15,12 +15,17 @@
*/
package org.springframework.data.jdbc.core.convert;
import java.sql.Array;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;
/**
* Maps a {@link ResultSet} to an entity of type {@code T}, including entities referenced. This {@link RowMapper} might
@@ -41,8 +46,6 @@ public class EntityRowMapper<T> implements RowMapper<T> {
private final Identifier identifier;
/**
*
*
* @deprecated use {@link EntityRowMapper#EntityRowMapper(AggregatePath, JdbcConverter, Identifier)} instead
*/
@Deprecated(since = "3.2", forRemoval = true)
@@ -73,11 +76,41 @@ public class EntityRowMapper<T> implements RowMapper<T> {
}
@Override
public T mapRow(ResultSet resultSet, int rowNumber) {
public T mapRow(ResultSet resultSet, int rowNumber) throws SQLException {
RowDocument document = toRowDocument(resultSet);
// TODO: Remove mapRow methods.
if (true) {
return path == null //
? converter.readAndResolve(entity.getType(), document) //
: converter.readAndResolve(entity.getType(), document, identifier);
}
return path == null //
? converter.mapRow(entity, resultSet, rowNumber) //
: converter.mapRow(path, resultSet, identifier, rowNumber);
}
/**
* Create a {@link RowDocument} from the current {@link ResultSet} row.
*
* @param resultSet must not be {@literal null}.
* @return
* @throws SQLException
*/
static RowDocument toRowDocument(ResultSet resultSet) throws SQLException {
ResultSetMetaData md = resultSet.getMetaData();
int columnCount = md.getColumnCount();
RowDocument document = new RowDocument(columnCount);
for (int i = 0; i < columnCount; i++) {
Object rsv = JdbcUtils.getResultSetValue(resultSet, i + 1);
String columnName = md.getColumnLabel(i + 1);
document.put(columnName, rsv instanceof Array a ? a.getArray() : rsv);
}
return document;
}
}

View File

@@ -69,6 +69,8 @@ public final class Identifier {
Assert.notNull(name, "Name must not be empty");
Assert.notNull(targetType, "Target type must not be null");
// TODO: Is value allowed to be null? SingleIdentifierValue says so, but this type doesn't allows it and
// SqlParametersFactory.lambda$forQueryByIdentifier$1 fails with a NPE.
return new Identifier(Collections.singletonList(new SingleIdentifierValue(name, value, targetType)));
}
@@ -173,6 +175,18 @@ public final class Identifier {
return this.parts.size();
}
@Nullable
public Object get(SqlIdentifier columnName) {
for (SingleIdentifierValue part : parts) {
if (part.getName().equals(columnName)) {
return part.getValue();
}
}
return null;
}
/**
* A single value of an Identifier consisting of the column name, the value and the target type which is to be used to
* store the element in the database.
@@ -274,8 +288,10 @@ public final class Identifier {
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Identifier that = (Identifier) o;
return Objects.equals(parts, that.parts);
}

View File

@@ -1,32 +1,32 @@
/*
* Copyright 2019-2023 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
*
* https://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.
*/
/*
* Copyright 2019-2023 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
*
* https://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.jdbc.core.convert;
import java.sql.ResultSet;
import java.sql.SQLType;
import org.springframework.data.jdbc.core.mapping.JdbcValue;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.lang.Nullable;
/**
@@ -73,7 +73,7 @@ public interface JdbcConverter extends RelationalConverter {
* @deprecated use {@link #mapRow(AggregatePath, ResultSet, Identifier, Object)} instead.
*/
@Deprecated(since = "3.2", forRemoval = true)
default <T> T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key){
default <T> T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key) {
return mapRow(path.getAggregatePath(), resultSet, identifier, key);
};
@@ -89,6 +89,49 @@ public interface JdbcConverter extends RelationalConverter {
*/
<T> T mapRow(AggregatePath path, ResultSet resultSet, Identifier identifier, Object key);
/**
* Apply a projection to {@link RowDocument} and return the projection return type {@code R}.
* {@link EntityProjection#isProjection() Non-projecting} descriptors fall back to {@link #read(Class, RowDocument)
* regular object materialization}.
*
* @param descriptor the projection descriptor, must not be {@literal null}.
* @param document must not be {@literal null}.
* @param <R>
* @return a new instance of the projection return type {@code R}.
* @since 3.2
* @see #project(EntityProjection, RowDocument)
*/
<R> R projectAndResolve(EntityProjection<R, ?> descriptor, RowDocument document);
/**
* Read a {@link RowDocument} into the requested {@link Class aggregate type} and resolve references by looking these
* up from {@link RelationResolver}.
*
* @param type target aggregate type.
* @param source source {@link RowDocument}.
* @return the converted object.
* @param <R> aggregate type.
* @since 3.2
* @see #read(Class, RowDocument)
*/
default <R> R readAndResolve(Class<R> type, RowDocument source) {
return readAndResolve(type, source, Identifier.empty());
}
/**
* Read a {@link RowDocument} into the requested {@link Class aggregate type} and resolve references by looking these
* up from {@link RelationResolver}.
*
* @param type target aggregate type.
* @param source source {@link RowDocument}.
* @param identifier identifier chain.
* @return the converted object.
* @param <R> aggregate type.
* @since 3.2
* @see #read(Class, RowDocument)
*/
<R> R readAndResolve(Class<R> type, RowDocument source, Identifier identifier);
/**
* The type to be used to store this property in the database. Multidimensional arrays are unwrapped to reflect a
* top-level array type (e.g. {@code String[][]} returns {@code String[]}).
@@ -110,4 +153,5 @@ public interface JdbcConverter extends RelationalConverter {
@Override
RelationalMappingContext getMappingContext();
}

View File

@@ -21,9 +21,8 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.jdbc.core.RowMapper;
/**
@@ -56,7 +55,14 @@ class MapEntityRowMapper<T> implements RowMapper<Map.Entry<Object, T>> {
return new HashMap.SimpleEntry<>(key, mapEntity(rs, key));
}
private T mapEntity(ResultSet resultSet, Object key) {
private T mapEntity(ResultSet resultSet, Object key) throws SQLException {
if (true) {
RowDocument document = EntityRowMapper.toRowDocument(resultSet);
return (T) converter.readAndResolve(path.getLeafEntity().getType(), document,
identifier.withPart(keyColumn, key, Object.class));
}
return converter.mapRow(path, resultSet, identifier, key);
}
}

View File

@@ -1534,6 +1534,15 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(zeroValue);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [zeroValue='").append(zeroValue).append('\'');
sb.append(']');
return sb.toString();
}
}
static class NoIdListChain1 {
@@ -1554,6 +1563,16 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(oneValue, chain0);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [oneValue='").append(oneValue).append('\'');
sb.append(", chain0=").append(chain0);
sb.append(']');
return sb.toString();
}
}
static class NoIdListChain2 {
@@ -1574,6 +1593,16 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(twoValue, chain1);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [twoValue='").append(twoValue).append('\'');
sb.append(", chain1=").append(chain1);
sb.append(']');
return sb.toString();
}
}
static class NoIdListChain3 {
@@ -1594,6 +1623,16 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(threeValue, chain2);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [threeValue='").append(threeValue).append('\'');
sb.append(", chain2=").append(chain2);
sb.append(']');
return sb.toString();
}
}
static class NoIdListChain4 {
@@ -1616,6 +1655,18 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(four, fourValue, chain3);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [four=").append(four);
sb.append(", fourValue='").append(fourValue).append('\'');
sb.append(", chain3=").append(chain3);
sb.append(']');
return sb.toString();
}
}
/**
@@ -1638,6 +1689,15 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(zeroValue);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [zeroValue='").append(zeroValue).append('\'');
sb.append(']');
return sb.toString();
}
}
static class NoIdMapChain1 {
@@ -1658,6 +1718,16 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(oneValue, chain0);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [oneValue='").append(oneValue).append('\'');
sb.append(", chain0=").append(chain0);
sb.append(']');
return sb.toString();
}
}
static class NoIdMapChain2 {
@@ -1678,6 +1748,16 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(twoValue, chain1);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [twoValue='").append(twoValue).append('\'');
sb.append(", chain1=").append(chain1);
sb.append(']');
return sb.toString();
}
}
static class NoIdMapChain3 {
@@ -1698,6 +1778,16 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(threeValue, chain2);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [threeValue='").append(threeValue).append('\'');
sb.append(", chain2=").append(chain2);
sb.append(']');
return sb.toString();
}
}
static class NoIdMapChain4 {
@@ -1720,6 +1810,17 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
public int hashCode() {
return Objects.hash(four, fourValue, chain3);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [four=").append(four);
sb.append(", fourValue='").append(fourValue).append('\'');
sb.append(", chain3=").append(chain3);
sb.append(']');
return sb.toString();
}
}
@SuppressWarnings("unused")

View File

@@ -302,7 +302,7 @@ public class BasicRelationalConverter implements RelationalConverter {
return value;
}
if (Enum.class.isAssignableFrom(target)) {
if (Enum.class.isAssignableFrom(target) && value instanceof CharSequence) {
return Enum.valueOf((Class<Enum>) target, value.toString());
}

View File

@@ -49,12 +49,14 @@ import org.springframework.data.projection.EntityProjectionIntrospector;
import org.springframework.data.projection.EntityProjectionIntrospector.ProjectionPredicate;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
import org.springframework.data.relational.core.mapping.PersistentPropertyTranslator;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.data.util.Predicates;
import org.springframework.data.util.TypeInformation;
@@ -161,14 +163,17 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
return (R) read(typeToRead, document);
}
ProjectingConversionContext context = new ProjectingConversionContext(this, getConversions(), ObjectPath.ROOT,
this::readCollectionOrArray, this::readMap, this::getPotentiallyConvertedSimpleRead, projection);
ProjectingConversionContext context = newProjectingConversionContext(projection);
return doReadProjection(context, document, projection);
}
protected <R> ProjectingConversionContext newProjectingConversionContext(EntityProjection<R, ?> projection) {
return new ProjectingConversionContext(this, getConversions(), ObjectPath.ROOT, this::readCollectionOrArray,
this::readMap, this::getPotentiallyConvertedSimpleRead, projection);
}
@SuppressWarnings("unchecked")
private <R> R doReadProjection(ConversionContext context, RowDocument document, EntityProjection<R, ?> projection) {
protected <R> R doReadProjection(ConversionContext context, RowDocument document, EntityProjection<R, ?> projection) {
RelationalPersistentEntity<?> entity = getMappingContext()
.getRequiredPersistentEntity(projection.getActualDomainType());
@@ -186,8 +191,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
PersistentPropertyAccessor<?> convertingAccessor = PropertyTranslatingPropertyAccessor
.create(new ConvertingPropertyAccessor<>(accessor, getConversionService()), propertyTranslator);
RelationalPropertyValueProvider valueProvider = new RelationalPropertyValueProvider(context, documentAccessor,
evaluator, spELContext);
RelationalPropertyValueProvider valueProvider = newValueProvider(documentAccessor, evaluator, context);
readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, Predicates.isTrue());
return (R) projectionFactory.createProjection(mappedType.getType(), accessor.getBean());
@@ -224,8 +228,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
PersistentPropertyAccessor<?> convertingAccessor = new ConvertingPropertyAccessor<>(accessor,
getConversionService());
RelationalPropertyValueProvider valueProvider = new RelationalPropertyValueProvider(context, documentAccessor,
evaluator, spELContext);
RelationalPropertyValueProvider valueProvider = newValueProvider(documentAccessor, evaluator, context);
readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, Predicates.isTrue());
@@ -290,19 +293,33 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
@SuppressWarnings("unchecked")
protected <S extends Object> S readAggregate(ConversionContext context, RowDocument document,
TypeInformation<? extends S> typeHint) {
return readAggregate(context, new RowDocumentAccessor(document), typeHint);
}
/**
* Conversion method to materialize an object from a {@link RowDocument document}. Can be overridden by subclasses.
*
* @param context must not be {@literal null}
* @param documentAccessor must not be {@literal null}
* @param typeHint the {@link TypeInformation} to be used to unmarshall this {@link RowDocument}.
* @return the converted object, will never be {@literal null}.
*/
@SuppressWarnings("unchecked")
protected <S extends Object> S readAggregate(ConversionContext context, RowDocumentAccessor documentAccessor,
TypeInformation<? extends S> typeHint) {
Class<? extends S> rawType = typeHint.getType();
if (getConversions().hasCustomReadTarget(document.getClass(), rawType)) {
return doConvert(document, rawType, typeHint.getType());
if (getConversions().hasCustomReadTarget(documentAccessor.getClass(), rawType)) {
return doConvert(documentAccessor, rawType, typeHint.getType());
}
if (RowDocument.class.isAssignableFrom(rawType)) {
return (S) document;
return (S) documentAccessor;
}
if (typeHint.isMap()) {
return context.convert(document, typeHint);
return context.convert(documentAccessor, typeHint);
}
RelationalPersistentEntity<?> entity = getMappingContext().getPersistentEntity(rawType);
@@ -310,10 +327,10 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
if (entity == null) {
throw new MappingException(
String.format("Expected to read Document %s into type %s but didn't find a PersistentEntity for the latter",
document, rawType));
documentAccessor, rawType));
}
return read(context, (RelationalPersistentEntity<S>) entity, document);
return read(context, (RelationalPersistentEntity<S>) entity, documentAccessor);
}
/**
@@ -364,7 +381,6 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
* @param targetType the {@link Map} {@link TypeInformation} to be used to unmarshall this {@link RowDocument}.
* @return the converted {@link Collection} or array, will never be {@literal null}.
*/
@SuppressWarnings("unchecked")
protected Object readCollectionOrArray(ConversionContext context, Collection<?> source,
TypeInformation<?> targetType) {
@@ -408,10 +424,10 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
return getConversionService().convert(value, fallback);
}
private <S> S read(ConversionContext context, RelationalPersistentEntity<S> entity, RowDocument document) {
private <S> S read(ConversionContext context, RelationalPersistentEntity<S> entity,
RowDocumentAccessor documentAccessor) {
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(document, spELContext);
RowDocumentAccessor documentAccessor = new RowDocumentAccessor(document);
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(documentAccessor.getDocument(), spELContext);
InstanceCreatorMetadata<RelationalPersistentProperty> instanceCreatorMetadata = entity.getInstanceCreatorMetadata();
@@ -432,12 +448,37 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
private ParameterValueProvider<RelationalPersistentProperty> getParameterProvider(ConversionContext context,
RelationalPersistentEntity<?> entity, RowDocumentAccessor source, SpELExpressionEvaluator evaluator) {
RelationalPropertyValueProvider provider = new RelationalPropertyValueProvider(context, source, evaluator,
spELContext);
// Ensure that ConversionContext is contextualized to the current property.
RelationalPropertyValueProvider contextualizing = new RelationalPropertyValueProvider() {
@Override
public boolean hasValue(RelationalPersistentProperty property) {
return withContext(context.forProperty(property)).hasValue(property);
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public <T> T getPropertyValue(RelationalPersistentProperty property) {
ConversionContext propertyContext = context.forProperty(property);
RelationalPropertyValueProvider provider = withContext(propertyContext);
if (property.isEmbedded()) {
return (T) readEmbedded(propertyContext, provider, source, property,
getMappingContext().getRequiredPersistentEntity(property));
}
return provider.getPropertyValue(property);
}
@Override
public RelationalPropertyValueProvider withContext(ConversionContext context) {
return newValueProvider(source, evaluator, context);
}
};
// TODO: Add support for enclosing object (non-static inner classes)
PersistentEntityParameterValueProvider<RelationalPersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>(
entity, provider, context.getPath().getCurrentObject());
entity, contextualizing, context.getPath().getCurrentObject());
return new ConverterAwareSpELExpressionParameterValueProvider(context, evaluator, getConversionService(),
parameterProvider);
@@ -453,8 +494,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
ObjectPath currentPath = context.getPath().push(accessor.getBean(), entity);
ConversionContext contextToUse = context.withPath(currentPath);
RelationalPropertyValueProvider valueProvider = new RelationalPropertyValueProvider(contextToUse, documentAccessor,
evaluator, spELContext);
RelationalPropertyValueProvider valueProvider = newValueProvider(documentAccessor, evaluator, contextToUse);
Predicate<RelationalPersistentProperty> propertyFilter = isConstructorArgument(entity).negate();
readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, propertyFilter);
@@ -462,56 +502,72 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
return accessor.getBean();
}
protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor,
SpELExpressionEvaluator evaluator, ConversionContext context) {
return new DocumentValueProvider(context, documentAccessor, evaluator, spELContext);
}
private void readProperties(ConversionContext context, RelationalPersistentEntity<?> entity,
PersistentPropertyAccessor<?> accessor, RowDocumentAccessor documentAccessor,
RelationalPropertyValueProvider valueProvider, Predicate<RelationalPersistentProperty> propertyFilter) {
for (RelationalPersistentProperty prop : entity) {
for (RelationalPersistentProperty property : entity) {
if (!propertyFilter.test(prop)) {
if (!propertyFilter.test(property)) {
continue;
}
ConversionContext propertyContext = context.forProperty(prop);
ConversionContext propertyContext = context.forProperty(property);
RelationalPropertyValueProvider valueProviderToUse = valueProvider.withContext(propertyContext);
if (prop.isAssociation()) {
// TODO: Read AggregateReference
if (property.isEmbedded()) {
accessor.setProperty(property, readEmbedded(propertyContext, valueProviderToUse, documentAccessor, property,
getMappingContext().getRequiredPersistentEntity(property)));
continue;
}
if (prop.isEmbedded()) {
accessor.setProperty(prop, readEmbedded(propertyContext, documentAccessor, prop,
getMappingContext().getRequiredPersistentEntity(prop)));
if (!valueProviderToUse.hasValue(property)) {
continue;
}
if (!documentAccessor.hasValue(prop)) {
continue;
}
accessor.setProperty(prop, valueProviderToUse.getPropertyValue(prop));
accessor.setProperty(property, valueProviderToUse.getPropertyValue(property));
}
}
@Nullable
private Object readEmbedded(ConversionContext context, RowDocumentAccessor documentAccessor,
RelationalPersistentProperty prop, RelationalPersistentEntity<?> unwrappedEntity) {
private Object readEmbedded(ConversionContext conversionContext, RelationalPropertyValueProvider provider,
RowDocumentAccessor source, RelationalPersistentProperty property,
RelationalPersistentEntity<?> persistentEntity) {
if (prop.findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_EMPTY)) {
return read(context, unwrappedEntity, documentAccessor.getDocument());
}
for (RelationalPersistentProperty persistentProperty : unwrappedEntity) {
if (documentAccessor.hasValue(persistentProperty)) {
return read(context, unwrappedEntity, documentAccessor.getDocument());
}
if (shouldReadEmbeddable(conversionContext, property, persistentEntity, provider)) {
return read(conversionContext, persistentEntity, source);
}
return null;
}
private boolean shouldReadEmbeddable(ConversionContext context, RelationalPersistentProperty property,
RelationalPersistentEntity<?> unwrappedEntity, RelationalPropertyValueProvider propertyValueProvider) {
OnEmpty onEmpty = property.getRequiredAnnotation(Embedded.class).onEmpty();
if (onEmpty.equals(OnEmpty.USE_EMPTY)) {
return true;
}
for (RelationalPersistentProperty persistentProperty : unwrappedEntity) {
RelationalPropertyValueProvider contextual = propertyValueProvider
.withContext(context.forProperty(persistentProperty));
if (contextual.hasValue(persistentProperty)) {
return true;
}
}
return false;
}
static Predicate<RelationalPersistentProperty> isConstructorArgument(PersistentEntity<?, ?> entity) {
return entity::isCreatorArgument;
}
@@ -532,7 +588,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
final ContainerValueConverter<Map<?, ?>> mapConverter;
final ValueConverter<Object> elementConverter;
DefaultConversionContext(RelationalConverter sourceConverter,
protected DefaultConversionContext(RelationalConverter sourceConverter,
org.springframework.data.convert.CustomConversions customConversions, ObjectPath objectPath,
ContainerValueConverter<RowDocument> documentConverter,
ContainerValueConverter<Collection<?>> collectionConverter, ContainerValueConverter<Map<?, ?>> mapConverter,
@@ -616,7 +672,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
*
* @param <T>
*/
interface ValueConverter<T> {
public interface ValueConverter<T> {
Object convert(T source, TypeInformation<?> typeHint);
@@ -628,7 +684,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
*
* @param <T>
*/
interface ContainerValueConverter<T> {
public interface ContainerValueConverter<T> {
Object convert(ConversionContext context, T source, TypeInformation<?> typeHint);
@@ -639,11 +695,11 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
/**
* @since 3.4.3
*/
class ProjectingConversionContext extends DefaultConversionContext {
protected class ProjectingConversionContext extends DefaultConversionContext {
private final EntityProjection<?, ?> returnedTypeDescriptor;
ProjectingConversionContext(RelationalConverter sourceConverter, CustomConversions customConversions,
protected ProjectingConversionContext(RelationalConverter sourceConverter, CustomConversions customConversions,
ObjectPath path, ContainerValueConverter<Collection<?>> collectionConverter,
ContainerValueConverter<Map<?, ?>> mapConverter, ValueConverter<Object> elementConverter,
EntityProjection<?, ?> projection) {
@@ -749,6 +805,61 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
}
}
// TODO: Docs
protected interface RelationalPropertyValueProvider extends PropertyValueProvider<RelationalPersistentProperty> {
/**
* Determine whether there is a value for the given {@link RelationalPersistentProperty}.
*
* @param property
* @return
*/
boolean hasValue(RelationalPersistentProperty property);
/**
* Contextualize this property value provider.
*
* @param context
* @return
*/
RelationalPropertyValueProvider withContext(ConversionContext context);
}
/**
* {@link RelationalPropertyValueProvider} extension to obtain values for {@link AggregatePath}s.
*/
protected interface AggregatePathValueProvider extends RelationalPropertyValueProvider {
/**
* Determine whether there is a value for the given {@link AggregatePath}.
*
* @param path
* @return
*/
boolean hasValue(AggregatePath path);
boolean hasValue(SqlIdentifier identifier);
/**
* Returns a value for the given {@link AggregatePath}.
*
* @param path will never be {@literal null}.
* @return
*/
@Nullable
Object getValue(AggregatePath path);
/**
* Contextualize this property value provider.
*
* @param context
* @return
*/
@Override
AggregatePathValueProvider withContext(ConversionContext context);
}
/**
* {@link PropertyValueProvider} to evaluate a SpEL expression if present on the property or simply accesses the field
* of the configured source {@link RowDocument}.
@@ -757,9 +868,9 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
* @author Mark Paluch
* @author Christoph Strobl
*/
record RelationalPropertyValueProvider(ConversionContext context, RowDocumentAccessor accessor,
protected record DocumentValueProvider(ConversionContext context, RowDocumentAccessor accessor,
SpELExpressionEvaluator evaluator,
SpELContext spELContext) implements PropertyValueProvider<RelationalPersistentProperty> {
SpELContext spELContext) implements RelationalPropertyValueProvider, AggregatePathValueProvider {
/**
* Creates a new {@link RelationalPropertyValueProvider} for the given source and {@link SpELExpressionEvaluator}.
@@ -768,13 +879,14 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
* @param accessor must not be {@literal null}.
* @param evaluator must not be {@literal null}.
*/
RelationalPropertyValueProvider {
protected DocumentValueProvider {
Assert.notNull(context, "ConversionContext must no be null");
Assert.notNull(accessor, "DocumentAccessor must no be null");
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null");
}
@Override
@Nullable
@SuppressWarnings("unchecked")
public <T> T getPropertyValue(RelationalPersistentProperty property) {
@@ -791,11 +903,39 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme
return (T) contextToUse.convert(value, property.getTypeInformation());
}
public RelationalPropertyValueProvider withContext(ConversionContext context) {
return context == this.context ? this
: new RelationalPropertyValueProvider(context, accessor, evaluator, spELContext);
@Override
public boolean hasValue(RelationalPersistentProperty property) {
return accessor.hasValue(property);
}
@Nullable
@Override
public Object getValue(AggregatePath path) {
Object value = accessor.document().get(path.getColumnInfo().alias().getReference());
if (value == null) {
return null;
}
return context.convert(value, path.getRequiredLeafProperty().getTypeInformation());
}
@Override
public boolean hasValue(AggregatePath path) {
return accessor.document().get(path.getColumnInfo().alias().getReference()) != null;
}
@Override
public boolean hasValue(SqlIdentifier identifier) {
return accessor().document().get(identifier.getReference()) != null;
}
@Override
public DocumentValueProvider withContext(ConversionContext context) {
return context == this.context ? this : new DocumentValueProvider(context, accessor, evaluator, spELContext);
}
}
/**

View File

@@ -36,9 +36,9 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
* @since 3.2
*/
class ObjectPath {
public final class ObjectPath {
static final ObjectPath ROOT = new ObjectPath();
public static final ObjectPath ROOT = new ObjectPath();
private final @Nullable ObjectPath parent;
private final @Nullable Object object;

View File

@@ -30,7 +30,7 @@ import org.springframework.util.Assert;
* @author Mark Paluch
* @since 3.2
*/
class RowDocumentAccessor {
public class RowDocumentAccessor {
private final RowDocument document;
@@ -103,7 +103,7 @@ class RowDocumentAccessor {
Assert.notNull(property, "Property must not be null");
return document.containsKey(getColumnName(property));
return document.get(getColumnName(property)) != null;
}
String getColumnName(RelationalPersistentProperty prop) {

View File

@@ -42,10 +42,14 @@ public class RowDocument implements Map<String, Object> {
this.delegate = new LinkedCaseInsensitiveMap<>();
}
public RowDocument(int expectedSize) {
this.delegate = new LinkedCaseInsensitiveMap<>(expectedSize);
}
public RowDocument(Map<String, ? extends Object> map) {
this.delegate = new LinkedCaseInsensitiveMap<>();
this.delegate.putAll(delegate);
this.delegate.putAll(map);
}
/**