diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java index 64093900..c5712523 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java @@ -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 mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key) { - return new ReadingContext(getMappingContext().getAggregatePath( entity), - new ResultSetAccessor(resultSet), Identifier.empty(), key).mapRow(); + return new ReadingContext(getMappingContext().getAggregatePath(entity), new ResultSetAccessor(resultSet), + Identifier.empty(), key).mapRow(); } - @Override public T mapRow(AggregatePath path, ResultSet resultSet, Identifier identifier, Object key) { return new ReadingContext(path, new ResultSetAccessor(resultSet), identifier, key).mapRow(); } + @Override + public R projectAndResolve(EntityProjection 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 readAndResolve(Class type, RowDocument source, Identifier identifier) { + + RelationalPersistentEntity entity = (RelationalPersistentEntity) 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 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 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 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 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 convert(Object source, TypeInformation typeHint) { + return delegate.convert(source, typeHint); + } + + @Override + public S convert(Object source, TypeInformation 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 entity = (RelationalPersistentEntity) 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 entity, AggregatePath rootPath, - AggregatePath path, Identifier identifier, Object key, - JdbcPropertyValueProvider propertyValueProvider, + private ReadingContext(RelationalPersistentEntity 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) 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() { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java index fe6d0b7a..15683746 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java @@ -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 implements RowMapper { 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 implements RowMapper { } @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; + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java index 8f7eff1a..f1b803be 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java @@ -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); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java index 3de2ceb2..f7c48304 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java @@ -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 mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key){ + default 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 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 + * @return a new instance of the projection return type {@code R}. + * @since 3.2 + * @see #project(EntityProjection, RowDocument) + */ + R projectAndResolve(EntityProjection 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 aggregate type. + * @since 3.2 + * @see #read(Class, RowDocument) + */ + default R readAndResolve(Class 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 aggregate type. + * @since 3.2 + * @see #read(Class, RowDocument) + */ + R readAndResolve(Class 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(); + } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java index 39e70e2a..2ec77afe 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java @@ -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 implements RowMapper> { 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); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java index e337383f..99f505b8 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java @@ -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") diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java index 00c435e4..05ffb29a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java @@ -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) target, value.toString()); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java index 66b42a8d..30aaac0c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java @@ -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 ProjectingConversionContext newProjectingConversionContext(EntityProjection projection) { + return new ProjectingConversionContext(this, getConversions(), ObjectPath.ROOT, this::readCollectionOrArray, + this::readMap, this::getPotentiallyConvertedSimpleRead, projection); + } + @SuppressWarnings("unchecked") - private R doReadProjection(ConversionContext context, RowDocument document, EntityProjection projection) { + protected R doReadProjection(ConversionContext context, RowDocument document, EntityProjection 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 readAggregate(ConversionContext context, RowDocument document, TypeInformation 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 readAggregate(ConversionContext context, RowDocumentAccessor documentAccessor, + TypeInformation typeHint) { Class 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) entity, document); + return read(context, (RelationalPersistentEntity) 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 read(ConversionContext context, RelationalPersistentEntity entity, RowDocument document) { + private S read(ConversionContext context, RelationalPersistentEntity entity, + RowDocumentAccessor documentAccessor) { - SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(document, spELContext); - RowDocumentAccessor documentAccessor = new RowDocumentAccessor(document); + SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(documentAccessor.getDocument(), spELContext); InstanceCreatorMetadata instanceCreatorMetadata = entity.getInstanceCreatorMetadata(); @@ -432,12 +448,37 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme private ParameterValueProvider 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 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 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 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 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 isConstructorArgument(PersistentEntity entity) { return entity::isCreatorArgument; } @@ -532,7 +588,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme final ContainerValueConverter> mapConverter; final ValueConverter elementConverter; - DefaultConversionContext(RelationalConverter sourceConverter, + protected DefaultConversionContext(RelationalConverter sourceConverter, org.springframework.data.convert.CustomConversions customConversions, ObjectPath objectPath, ContainerValueConverter documentConverter, ContainerValueConverter> collectionConverter, ContainerValueConverter> mapConverter, @@ -616,7 +672,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme * * @param */ - interface ValueConverter { + public interface ValueConverter { Object convert(T source, TypeInformation typeHint); @@ -628,7 +684,7 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme * * @param */ - interface ContainerValueConverter { + public interface ContainerValueConverter { 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> collectionConverter, ContainerValueConverter> mapConverter, ValueConverter elementConverter, EntityProjection projection) { @@ -749,6 +805,61 @@ public class MappingRelationalConverter extends BasicRelationalConverter impleme } } + // TODO: Docs + protected interface RelationalPropertyValueProvider extends PropertyValueProvider { + + /** + * 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 { + 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 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); + } + } /** diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/ObjectPath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/ObjectPath.java index 8fc2248e..8d0925a5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/ObjectPath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/ObjectPath.java @@ -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; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RowDocumentAccessor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RowDocumentAccessor.java index 4aa62c76..5d991c10 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RowDocumentAccessor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RowDocumentAccessor.java @@ -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) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/RowDocument.java b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/RowDocument.java index 38d77b2a..7c69413f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/RowDocument.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/RowDocument.java @@ -42,10 +42,14 @@ public class RowDocument implements Map { this.delegate = new LinkedCaseInsensitiveMap<>(); } + public RowDocument(int expectedSize) { + this.delegate = new LinkedCaseInsensitiveMap<>(expectedSize); + } + public RowDocument(Map map) { this.delegate = new LinkedCaseInsensitiveMap<>(); - this.delegate.putAll(delegate); + this.delegate.putAll(map); } /**