From 632b31f6e5767e76efe9b4b51198072e3f2459b6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Sep 2023 09:57:44 +0200 Subject: [PATCH 1/6] Prepare issue branch. --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-r2dbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index d9ee91c910..bf4a8ee0ad 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1554-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 271486f02a..0fb77c849e 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1554-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 2ac75e7993..740ac8196f 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.2.0-SNAPSHOT + 3.2.0-GH-1554-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1554-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index d65f06a082..64b4fb1fb3 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.2.0-SNAPSHOT + 3.2.0-GH-1554-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1554-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 74f350faa8..f359cc662e 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.2.0-SNAPSHOT + 3.2.0-GH-1554-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-GH-1554-SNAPSHOT From 6b8ecbedf89906e46f77a6f6ae85a6bb4870bcb3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Sep 2023 10:00:00 +0200 Subject: [PATCH 2/6] Add support for multi-level projections using EntityProjection. We now support multi-level projections by introspecting the result and the domain type and read projections directly into a DTO or a backing map for interface projections. --- .../conversion/BasicRelationalConverter.java | 17 ++ .../MappingRelationalConverter.java | 235 +++++++++++++++++- .../core/conversion/RelationalConverter.java | 36 +++ .../core/conversion/RowDocumentAccessor.java | 32 ++- .../BasicRelationalPersistentProperty.java | 9 + .../EmbeddedRelationalPersistentProperty.java | 5 + .../mapping/PersistentPropertyTranslator.java | 92 +++++++ .../mapping/RelationalPersistentProperty.java | 8 + .../data/relational/domain/RowDocument.java | 17 +- .../MappingRelationalConverterUnitTests.java | 98 ++++++++ 10 files changed, 540 insertions(+), 9 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyTranslator.java 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 b9fb1f254a..00c435e49a 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 @@ -39,6 +39,8 @@ import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.projection.EntityProjection; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -126,6 +128,21 @@ public PersistentPropertyPathAccessor getPropertyAccessor(PersistentEntit return new ConvertingPropertyAccessor<>(accessor, conversionService); } + @Override + public EntityProjection introspectProjection(Class resultType, Class entityType) { + throw new UnsupportedOperationException(); + } + + @Override + public ProjectionFactory getProjectionFactory() { + throw new UnsupportedOperationException(); + } + + @Override + public R project(EntityProjection descriptor, RowDocument document) { + throw new UnsupportedOperationException(); + } + @Override public R read(Class type, RowDocument source) { throw new UnsupportedOperationException(); 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 82185320dc..66b42a8d52 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 @@ -17,10 +17,14 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.data.convert.CustomConversions; @@ -28,6 +32,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -39,12 +44,19 @@ 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.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.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.domain.RowDocument; +import org.springframework.data.util.Predicates; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -57,10 +69,14 @@ * @author Mark Paluch * @since 3.2 */ -public class MappingRelationalConverter extends BasicRelationalConverter { +public class MappingRelationalConverter extends BasicRelationalConverter implements ApplicationContextAware { private SpELContext spELContext; + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + + private final EntityProjectionIntrospector introspector; + /** * Creates a new {@link MappingRelationalConverter} given the new {@link RelationalMappingContext}. * @@ -71,6 +87,7 @@ public MappingRelationalConverter(RelationalMappingContext context) { super(context); this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE); + this.introspector = createIntrospector(projectionFactory, getConversions(), getMappingContext()); } /** @@ -85,6 +102,29 @@ public MappingRelationalConverter(RelationalMappingContext context, CustomConver super(context, conversions); this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE); + this.introspector = createIntrospector(projectionFactory, getConversions(), getMappingContext()); + + } + + private static EntityProjectionIntrospector createIntrospector(ProjectionFactory projectionFactory, + CustomConversions conversions, MappingContext mappingContext) { + + return EntityProjectionIntrospector.create(projectionFactory, + ProjectionPredicate.typeHierarchy().and((target, underlyingType) -> !conversions.isSimpleType(target)), + mappingContext); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + this.spELContext = new SpELContext(this.spELContext, applicationContext); + this.projectionFactory.setBeanFactory(applicationContext); + this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); + } + + @Override + public ProjectionFactory getProjectionFactory() { + return this.projectionFactory; } /** @@ -100,6 +140,128 @@ protected ConversionContext getConversionContext(ObjectPath path) { this::readMap, this::getPotentiallyConvertedSimpleRead); } + @Override + public EntityProjection introspectProjection(Class resultType, Class entityType) { + + RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(entityType); + if (persistentEntity == null && !resultType.isInterface() + || ClassUtils.isAssignable(RowDocument.class, resultType)) { + return (EntityProjection) EntityProjection.nonProjecting(resultType); + } + return introspector.introspect(resultType, entityType); + } + + @Override + public R project(EntityProjection projection, RowDocument document) { + + if (!projection.isProjection()) { // backed by real object + + TypeInformation typeToRead = projection.getMappedType().getType().isInterface() ? projection.getDomainType() + : projection.getMappedType(); + return (R) read(typeToRead, document); + } + + ProjectingConversionContext context = new ProjectingConversionContext(this, getConversions(), ObjectPath.ROOT, + this::readCollectionOrArray, this::readMap, this::getPotentiallyConvertedSimpleRead, projection); + + return doReadProjection(context, document, projection); + } + + @SuppressWarnings("unchecked") + private R doReadProjection(ConversionContext context, RowDocument document, EntityProjection projection) { + + RelationalPersistentEntity entity = getMappingContext() + .getRequiredPersistentEntity(projection.getActualDomainType()); + TypeInformation mappedType = projection.getActualMappedType(); + RelationalPersistentEntity mappedEntity = (RelationalPersistentEntity) getMappingContext() + .getPersistentEntity(mappedType); + SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(document, spELContext); + + boolean isInterfaceProjection = mappedType.getType().isInterface(); + if (isInterfaceProjection) { + + PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(mappedEntity); + RowDocumentAccessor documentAccessor = new RowDocumentAccessor(document); + PersistentPropertyAccessor accessor = new MapPersistentPropertyAccessor(); + + PersistentPropertyAccessor convertingAccessor = PropertyTranslatingPropertyAccessor + .create(new ConvertingPropertyAccessor<>(accessor, getConversionService()), propertyTranslator); + RelationalPropertyValueProvider valueProvider = new RelationalPropertyValueProvider(context, documentAccessor, + evaluator, spELContext); + + readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, Predicates.isTrue()); + return (R) projectionFactory.createProjection(mappedType.getType(), accessor.getBean()); + } + + // DTO projection + if (mappedEntity == null) { + throw new MappingException(String.format("No mapping metadata found for %s", mappedType.getType().getName())); + } + + // create target instance, merge metadata from underlying DTO type + PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(entity, + Predicates.negate(RelationalPersistentProperty::hasExplicitColumnName)); + RowDocumentAccessor documentAccessor = new RowDocumentAccessor(document) { + + @Override + String getColumnName(RelationalPersistentProperty prop) { + return propertyTranslator.translate(prop).getColumnName().getReference(); + } + }; + + InstanceCreatorMetadata instanceCreatorMetadata = mappedEntity + .getInstanceCreatorMetadata(); + ParameterValueProvider provider = instanceCreatorMetadata != null + && instanceCreatorMetadata.hasParameters() + ? getParameterProvider(context, mappedEntity, documentAccessor, evaluator) + : NoOpParameterValueProvider.INSTANCE; + + EntityInstantiator instantiator = getEntityInstantiators().getInstantiatorFor(mappedEntity); + R instance = instantiator.createInstance(mappedEntity, provider); + PersistentPropertyAccessor accessor = mappedEntity.getPropertyAccessor(instance); + + populateProperties(context, mappedEntity, documentAccessor, evaluator, instance); + + PersistentPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor<>(accessor, + getConversionService()); + RelationalPropertyValueProvider valueProvider = new RelationalPropertyValueProvider(context, documentAccessor, + evaluator, spELContext); + + readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, Predicates.isTrue()); + + return accessor.getBean(); + } + + private Object doReadOrProject(ConversionContext context, RowDocument source, TypeInformation typeHint, + EntityProjection typeDescriptor) { + + if (typeDescriptor.isProjection()) { + return doReadProjection(context, source, typeDescriptor); + } + + return readAggregate(context, source, typeHint); + } + + static class MapPersistentPropertyAccessor implements PersistentPropertyAccessor> { + + Map map = new LinkedHashMap<>(); + + @Override + public void setProperty(PersistentProperty persistentProperty, Object o) { + map.put(persistentProperty.getName(), o); + } + + @Override + public Object getProperty(PersistentProperty persistentProperty) { + return map.get(persistentProperty.getName()); + } + + @Override + public Map getBean() { + return map; + } + } + /** * Read a {@link RowDocument} into the requested {@link Class aggregate type}. * @@ -295,15 +457,14 @@ private S populateProperties(ConversionContext context, RelationalPersistent evaluator, spELContext); Predicate propertyFilter = isConstructorArgument(entity).negate(); - readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter); + readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, propertyFilter); return accessor.getBean(); } private void readProperties(ConversionContext context, RelationalPersistentEntity entity, PersistentPropertyAccessor accessor, RowDocumentAccessor documentAccessor, - RelationalPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator, - Predicate propertyFilter) { + RelationalPropertyValueProvider valueProvider, Predicate propertyFilter) { for (RelationalPersistentProperty prop : entity) { @@ -475,6 +636,44 @@ interface ContainerValueConverter { } + /** + * @since 3.4.3 + */ + class ProjectingConversionContext extends DefaultConversionContext { + + private final EntityProjection returnedTypeDescriptor; + + ProjectingConversionContext(RelationalConverter sourceConverter, CustomConversions customConversions, + ObjectPath path, ContainerValueConverter> collectionConverter, + ContainerValueConverter> mapConverter, ValueConverter elementConverter, + EntityProjection projection) { + super(sourceConverter, customConversions, path, + (context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection), + + collectionConverter, mapConverter, elementConverter); + this.returnedTypeDescriptor = projection; + } + + @Override + public ConversionContext forProperty(String name) { + + EntityProjection property = returnedTypeDescriptor.findProperty(name); + if (property == null) { + return new DefaultConversionContext(sourceConverter, conversions, objectPath, + MappingRelationalConverter.this::readAggregate, collectionConverter, mapConverter, elementConverter); + } + + return new ProjectingConversionContext(sourceConverter, conversions, objectPath, collectionConverter, + mapConverter, elementConverter, property); + } + + @Override + public ConversionContext withPath(ObjectPath currentPath) { + return new ProjectingConversionContext(sourceConverter, conversions, currentPath, collectionConverter, + mapConverter, elementConverter, returnedTypeDescriptor); + } + } + /** * Conversion context defining an interface for graph-traversal-based conversion of row documents. Entrypoint for * recursive conversion of {@link RowDocument} and other types. @@ -633,4 +832,32 @@ protected T potentiallyConvertSpelValue(Object object, Parameter (PersistentPropertyAccessor delegate, + PersistentPropertyTranslator propertyTranslator) implements PersistentPropertyAccessor { + + static PersistentPropertyAccessor create(PersistentPropertyAccessor delegate, + PersistentPropertyTranslator propertyTranslator) { + return new PropertyTranslatingPropertyAccessor<>(delegate, propertyTranslator); + } + + @Override + public void setProperty(PersistentProperty property, @Nullable Object value) { + delegate.setProperty(translate(property), value); + } + + @Override + public Object getProperty(PersistentProperty property) { + return delegate.getProperty(translate(property)); + } + + @Override + public T getBean() { + return delegate.getBean(); + } + + private RelationalPersistentProperty translate(PersistentProperty property) { + return propertyTranslator.translate((RelationalPersistentProperty) property); + } + } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java index 9cff6dc7ac..c78d9ea61e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java @@ -25,6 +25,9 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.projection.EntityProjection; +import org.springframework.data.projection.EntityProjectionIntrospector; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.RowDocument; @@ -54,6 +57,39 @@ public interface RelationalConverter { */ ConversionService getConversionService(); + /** + * Returns the {@link ProjectionFactory} for this converter. + * + * @return will never be {@literal null}. + * @since 3.2 + */ + ProjectionFactory getProjectionFactory(); + + /** + * Introspect the given {@link Class result type} in the context of the {@link Class entity type} whether the returned + * type is a projection and what property paths are participating in the projection. + * + * @param resultType the type to project on. Must not be {@literal null}. + * @param entityType the source domain type. Must not be {@literal null}. + * @return the introspection result. + * @since 3.2 + * @see EntityProjectionIntrospector#introspect(Class, Class) + */ + EntityProjection introspectProjection(Class resultType, Class entityType); + + /** + * 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 + */ + R project(EntityProjection descriptor, RowDocument document); + /** * Read a {@link RowDocument} into the requested {@link Class aggregate type}. * 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 74568aac85..4aa62c7694 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 @@ -15,6 +15,8 @@ */ package org.springframework.data.relational.core.conversion; +import java.util.Objects; + import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.RowDocument; import org.springframework.lang.Nullable; @@ -28,16 +30,19 @@ * @author Mark Paluch * @since 3.2 */ -record RowDocumentAccessor(RowDocument document) { +class RowDocumentAccessor { + + private final RowDocument document; /** * Creates a new {@link RowDocumentAccessor} for the given {@link RowDocument}. * * @param document must be a {@link RowDocument} effectively, must not be {@literal null}. */ - RowDocumentAccessor { + RowDocumentAccessor(RowDocument document) { Assert.notNull(document, "Document must not be null"); + this.document = document; } /** @@ -105,4 +110,27 @@ String getColumnName(RelationalPersistentProperty prop) { return prop.getColumnName().getReference(); } + public RowDocument document() { + return document; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (RowDocumentAccessor) obj; + return Objects.equals(this.document, that.document); + } + + @Override + public int hashCode() { + return Objects.hash(document); + } + + @Override + public String toString() { + return "RowDocumentAccessor[" + "document=" + document + ']'; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index b5f335c8c8..42d10c5d17 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -50,12 +50,14 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private final Lazy columnName; + private final boolean hasExplicitColumnName; private final @Nullable Expression columnNameExpression; private final Lazy> collectionIdColumnName; private final @Nullable Expression collectionIdColumnNameExpression; private final Lazy collectionKeyColumnName; private final @Nullable Expression collectionKeyColumnNameExpression; private final boolean isEmbedded; + private final String embeddedPrefix; private final NamingStrategy namingStrategy; private boolean forceQuote = true; @@ -128,6 +130,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value()) : createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); @@ -138,6 +141,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); this.columnNameExpression = null; } @@ -208,6 +212,11 @@ public SqlIdentifier getColumnName() { return createSqlIdentifier(expressionEvaluator.evaluate(columnNameExpression)); } + @Override + public boolean hasExplicitColumnName() { + return hasExplicitColumnName; + } + @Override public RelationalPersistentEntity getOwner() { return (RelationalPersistentEntity) super.getOwner(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java index a63f4335ad..cb39e3dafe 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java @@ -59,6 +59,11 @@ public SqlIdentifier getColumnName() { return delegate.getColumnName().transform(context::withEmbeddedPrefix); } + @Override + public boolean hasExplicitColumnName() { + return delegate.hasExplicitColumnName(); + } + @Override public RelationalPersistentEntity getOwner() { return delegate.getOwner(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyTranslator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyTranslator.java new file mode 100644 index 0000000000..bcd90d6ce2 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyTranslator.java @@ -0,0 +1,92 @@ +/* + * Copyright 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.relational.core.mapping; + +import java.util.function.Predicate; + +import org.springframework.data.util.Predicates; +import org.springframework.lang.Nullable; + +/** + * Utility to translate a {@link RelationalPersistentProperty} into a corresponding property from a different + * {@link RelationalPersistentEntity} by looking it up by name. + *

+ * Mainly used within the framework. + * + * @author Mark Paluch + * @since 3.2 + */ +public class PersistentPropertyTranslator { + + /** + * Translate a {@link RelationalPersistentProperty} into a corresponding property from a different + * {@link RelationalPersistentEntity}. + * + * @param property must not be {@literal null}. + * @return the translated property. Can be the original {@code property}. + */ + public RelationalPersistentProperty translate(RelationalPersistentProperty property) { + return property; + } + + /** + * Create a new {@link PersistentPropertyTranslator}. + * + * @param targetEntity must not be {@literal null}. + * @return the property translator to use. + */ + public static PersistentPropertyTranslator create(@Nullable RelationalPersistentEntity targetEntity) { + return create(targetEntity, Predicates.isTrue()); + } + + /** + * Create a new {@link PersistentPropertyTranslator} accepting a {@link Predicate filter predicate} whether the + * translation should happen at all. + * + * @param targetEntity must not be {@literal null}. + * @param translationFilter must not be {@literal null}. + * @return the property translator to use. + */ + public static PersistentPropertyTranslator create(@Nullable RelationalPersistentEntity targetEntity, + Predicate translationFilter) { + return targetEntity != null ? new EntityPropertyTranslator(targetEntity, translationFilter) + : new PersistentPropertyTranslator(); + } + + private static class EntityPropertyTranslator extends PersistentPropertyTranslator { + + private final RelationalPersistentEntity targetEntity; + private final Predicate translationFilter; + + EntityPropertyTranslator(RelationalPersistentEntity targetEntity, + Predicate translationFilter) { + this.targetEntity = targetEntity; + this.translationFilter = translationFilter; + } + + @Override + public RelationalPersistentProperty translate(RelationalPersistentProperty property) { + + if (!translationFilter.test(property)) { + return property; + } + + RelationalPersistentProperty targetProperty = targetEntity.getPersistentProperty(property.getName()); + return targetProperty != null ? targetProperty : property; + } + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java index 1b736f299a..afb5ede2c1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java @@ -35,6 +35,14 @@ public interface RelationalPersistentProperty extends PersistentProperty getOwner(); 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 b2295fbe52..38d77b2adf 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 @@ -24,6 +24,7 @@ import java.util.function.Function; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.ObjectUtils; @@ -47,7 +48,14 @@ public RowDocument(Map map) { this.delegate.putAll(delegate); } - public static Object of(String field, Object value) { + /** + * Factory method to create a RowDocument from a field and value. + * + * @param field the file name to use. + * @param value the value to use, can be {@literal null}. + * @return + */ + public static RowDocument of(String field, @Nullable Object value) { return new RowDocument().append(field, value); } @@ -131,7 +139,10 @@ public Object get(Object key) { @Nullable @Override - public Object put(String key, Object value) { + public Object put(String key, @Nullable Object value) { + + Assert.notNull(key, "Key must not be null!"); + return delegate.put(key, value); } @@ -142,7 +153,7 @@ public Object put(String key, Object value) { * @param value * @return */ - public RowDocument append(String key, Object value) { + public RowDocument append(String key, @Nullable Object value) { put(key, value); return this; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/MappingRelationalConverterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/MappingRelationalConverterUnitTests.java index 640b717307..b869fc1ee3 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/MappingRelationalConverterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/MappingRelationalConverterUnitTests.java @@ -17,18 +17,25 @@ import static org.assertj.core.api.Assertions.*; +import java.util.Collections; +import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.convert.ConverterBuilder; import org.springframework.data.convert.ConverterBuilder.ConverterAware; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.CustomConversions.StoreConversions; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.projection.EntityProjection; +import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.domain.RowDocument; @@ -161,6 +168,30 @@ void shouldApplyConverters() { assertThat(result.money.currency).isEqualTo("USD"); } + @Test // GH-1554 + void projectShouldReadNestedProjection() { + + RowDocument source = RowDocument.of("addresses", Collections.singletonList(RowDocument.of("s", "hwy"))); + + EntityProjection projection = converter + .introspectProjection(WithNestedProjection.class, Person.class); + WithNestedProjection person = converter.project(projection, source); + + assertThat(person.getAddresses()).extracting(AddressProjection::getStreet).hasSize(1).containsOnly("hwy"); + } + + @Test // GH-1554 + void projectShouldReadProjectionWithNestedEntity() { + + RowDocument source = RowDocument.of("addresses", Collections.singletonList(RowDocument.of("s", "hwy"))); + + EntityProjection projection = converter + .introspectProjection(ProjectionWithNestedEntity.class, Person.class); + ProjectionWithNestedEntity person = converter.project(projection, source); + + assertThat(person.getAddresses()).extracting(Address::getStreet).hasSize(1).containsOnly("hwy"); + } + static class SimpleType { @Id String id; @@ -230,4 +261,71 @@ static class WithEmbedded { @Embedded.Nullable(prefix = "simple_") SimpleType simple; } + static class Person { + + @Id String id; + + Date birthDate; + + @Column("foo") String firstname; + String lastname; + + Set

addresses; + + Person() { + + } + + @PersistenceCreator + public Person(Set
addresses) { + this.addresses = addresses; + } + } + + static class Address { + + @Column("s") String street; + String city; + + public String getStreet() { + return street; + } + + public String getCity() { + return city; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Address address = (Address) o; + return Objects.equals(street, address.street) && Objects.equals(city, address.city); + } + + @Override + public int hashCode() { + return Objects.hash(street, city); + } + } + + interface WithNestedProjection { + + Set getAddresses(); + } + + interface ProjectionWithNestedEntity { + + Set
getAddresses(); + } + + interface AddressProjection { + + String getStreet(); + } + } From e070d97ab088bb250b77b74f5b4b25855d2dde1d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Sep 2023 10:01:50 +0200 Subject: [PATCH 3/6] Migrate R2DBC to read RowDocument. --- .../r2dbc/convert/MappingR2dbcConverter.java | 54 ++++++++++++++++++- .../data/r2dbc/convert/R2dbcConverter.java | 14 +++++ .../data/r2dbc/convert/RowMetadataUtils.java | 14 ++++- .../DefaultReactiveDataAccessStrategy.java | 12 ++++- .../data/r2dbc/core/R2dbcEntityTemplate.java | 41 +++++++++----- .../core/ReactiveDataAccessStrategy.java | 14 +++++ .../ReactiveSelectOperationUnitTests.java | 40 +++++++++++++- 7 files changed, 169 insertions(+), 20 deletions(-) diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java index b0c51559d0..fe13e598de 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java @@ -18,6 +18,8 @@ import io.r2dbc.spi.Blob; import io.r2dbc.spi.Clob; import io.r2dbc.spi.ColumnMetadata; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -53,6 +55,7 @@ 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.domain.RowDocument; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.Parameter; @@ -75,7 +78,8 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements */ public MappingR2dbcConverter( MappingContext, ? extends RelationalPersistentProperty> context) { - super((RelationalMappingContext) context, new R2dbcCustomConversions(R2dbcCustomConversions.STORE_CONVERSIONS, Collections.emptyList())); + super((RelationalMappingContext) context, + new R2dbcCustomConversions(R2dbcCustomConversions.STORE_CONVERSIONS, Collections.emptyList())); } /** @@ -141,6 +145,54 @@ private R read(RelationalPersistentEntity entity, Row row, @Nullable RowM return result; } + @Override + public RowDocument toRowDocument(Class type, Readable row, Iterable metadata) { + + RowDocument document = new RowDocument(); + RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type); + + if (persistentEntity != null) { + captureRowValues(row, metadata, document, persistentEntity); + } + + for (ReadableMetadata m : metadata) { + + if (document.containsKey(m.getName())) { + continue; + } + + document.put(m.getName(), row.get(m.getName())); + } + + return document; + } + + private static void captureRowValues(Readable row, Iterable metadata, + RowDocument document, RelationalPersistentEntity persistentEntity) { + + for (RelationalPersistentProperty property : persistentEntity) { + + String identifier = property.getColumnName().getReference(); + + if (property.isEntity() || !RowMetadataUtils.containsColumn(metadata, identifier)) { + continue; + } + + Object value; + Class propertyType = property.getType(); + + if (propertyType.equals(Clob.class)) { + value = row.get(identifier, Clob.class); + } else if (propertyType.equals(Blob.class)) { + value = row.get(identifier, Blob.class); + } else { + value = row.get(identifier); + } + + document.put(identifier, value); + } + } + /** * Read a single value or a complete Entity from the {@link Row} passed as an argument. * diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java index 7d062c3156..a774641bbe 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java @@ -15,6 +15,8 @@ */ package org.springframework.data.r2dbc.convert; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -29,6 +31,7 @@ import org.springframework.data.relational.core.dialect.ArrayColumns; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.RowDocument; /** * Central R2DBC specific converter interface. @@ -103,4 +106,15 @@ public interface R2dbcConverter */ R read(Class type, Row source, RowMetadata metadata); + /** + * Create a flat {@link RowDocument} from a single {@link Readable Row or Stored Procedure output}. + * + * @param type the underlying entity type. + * @param row the row or stored procedure output to retrieve data from. + * @param metadata readable metadata. + * @return the {@link RowDocument} containing the data. + * @since 3.2 + */ + RowDocument toRowDocument(Class type, Readable row, Iterable metadata); + } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java index dfbb67b753..b4ea7dc1f4 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/RowMetadataUtils.java @@ -16,6 +16,7 @@ package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.ColumnMetadata; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.RowMetadata; /** @@ -34,10 +35,19 @@ class RowMetadataUtils { * @return {@code true} if the metadata contains the column {@code name}. */ public static boolean containsColumn(RowMetadata metadata, String name) { + return containsColumn(getColumnMetadata(metadata), name); + } - Iterable columns = getColumnMetadata(metadata); + /** + * Check whether the column {@code name} is contained in {@link RowMetadata}. The check happens case-insensitive. + * + * @param columns the metadata to inspect. + * @param name column name. + * @return {@code true} if the metadata contains the column {@code name}. + */ + public static boolean containsColumn(Iterable columns, String name) { - for (ColumnMetadata columnMetadata : columns) { + for (ReadableMetadata columnMetadata : columns) { if (name.equalsIgnoreCase(columnMetadata.getName())) { return true; } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java index 86eba3a714..4a066b6a17 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -15,6 +15,8 @@ */ package org.springframework.data.r2dbc.core; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -43,6 +45,7 @@ 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.lang.Nullable; import org.springframework.r2dbc.core.Parameter; import org.springframework.r2dbc.core.PreparedOperation; @@ -239,8 +242,7 @@ private Parameter getArrayValue(Parameter value, RelationalPersistentProperty pr return Parameter.empty(targetArrayType); } - return Parameter.fromOrEmpty(this.converter.getArrayValue(arrayColumns, property, value.getValue()), - actualType); + return Parameter.fromOrEmpty(this.converter.getArrayValue(arrayColumns, property, value.getValue()), actualType); } @Override @@ -253,6 +255,11 @@ public BiFunction getRowMapper(Class typeToRead) { return new EntityRowMapper<>(typeToRead, this.converter); } + @Override + public RowDocument toRowDocument(Class type, Readable row, Iterable metadata) { + return this.converter.toRowDocument(type, row, metadata); + } + @Override public PreparedOperation processNamedParameters(String query, NamedParameterProvider parameterProvider) { @@ -289,6 +296,7 @@ public StatementMapper getStatementMapper() { return this.statementMapper; } + @Override public R2dbcConverter getConverter() { return this.converter; } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java index f88283fe5e..68fa5e71f9 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java @@ -45,6 +45,7 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.r2dbc.convert.R2dbcConverter; @@ -66,6 +67,7 @@ import org.springframework.data.relational.core.sql.Functions; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.domain.RowDocument; import org.springframework.data.util.ProxyUtils; import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.DatabaseClient; @@ -95,6 +97,8 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw private final ReactiveDataAccessStrategy dataAccessStrategy; + private final R2dbcConverter converter; + private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; private final SpelAwareProxyProjectionFactory projectionFactory; @@ -116,7 +120,8 @@ public R2dbcEntityTemplate(ConnectionFactory connectionFactory) { this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) .bindMarkers(dialect.getBindMarkersFactory()).build(); this.dataAccessStrategy = new DefaultReactiveDataAccessStrategy(dialect); - this.mappingContext = dataAccessStrategy.getConverter().getMappingContext(); + this.converter = dataAccessStrategy.getConverter(); + this.mappingContext = converter.getMappingContext(); this.projectionFactory = new SpelAwareProxyProjectionFactory(); } @@ -157,6 +162,7 @@ public R2dbcEntityTemplate(DatabaseClient databaseClient, ReactiveDataAccessStra this.databaseClient = databaseClient; this.dataAccessStrategy = strategy; + this.converter = dataAccessStrategy.getConverter(); this.mappingContext = strategy.getConverter().getMappingContext(); this.projectionFactory = new SpelAwareProxyProjectionFactory(); } @@ -173,7 +179,7 @@ public ReactiveDataAccessStrategy getDataAccessStrategy() { @Override public R2dbcConverter getConverter() { - return this.dataAccessStrategy.getConverter(); + return this.converter; } @Override @@ -334,10 +340,10 @@ > P doSelect(Query query, Class entityClass, SqlIde return (P) ((Flux) result).concatMap(it -> maybeCallAfterConvert(it, tableName)); } - private RowsFetchSpec doSelect(Query query, Class entityClass, SqlIdentifier tableName, + private RowsFetchSpec doSelect(Query query, Class entityType, SqlIdentifier tableName, Class returnType) { - StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); + StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityType); StatementMapper.SelectSpec selectSpec = statementMapper // .createSelect(tableName) // @@ -362,7 +368,7 @@ private RowsFetchSpec doSelect(Query query, Class entityClass, SqlIden PreparedOperation operation = statementMapper.getMappedObject(selectSpec); - return getRowsFetchSpec(databaseClient.sql(operation), entityClass, returnType); + return getRowsFetchSpec(databaseClient.sql(operation), entityType, returnType); } @Override @@ -783,19 +789,26 @@ private List getSelectProjection(Table table, Query query, Class return query.getColumns().stream().map(table::column).collect(Collectors.toList()); } - private RowsFetchSpec getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class entityClass, - Class returnType) { + private RowsFetchSpec getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class entityType, + Class resultType) { - boolean simpleType; + boolean simpleType = getConverter().isSimpleType(resultType); BiFunction rowMapper; - if (returnType.isInterface()) { - simpleType = getConverter().isSimpleType(entityClass); - rowMapper = dataAccessStrategy.getRowMapper(entityClass) - .andThen(o -> projectionFactory.createProjection(returnType, o)); + + if (simpleType) { + rowMapper = dataAccessStrategy.getRowMapper(resultType); } else { - simpleType = getConverter().isSimpleType(returnType); - rowMapper = dataAccessStrategy.getRowMapper(returnType); + + EntityProjection projection = converter.introspectProjection(resultType, entityType); + + rowMapper = (row, rowMetadata) -> { + + RowDocument document = dataAccessStrategy.toRowDocument(resultType, row, rowMetadata.getColumnMetadatas()); + + return projection.isProjection() ? converter.project(projection, document) + : converter.read(resultType, document); + }; } // avoid top-level null values if the read type is a simple one (e.g. SELECT MAX(age) via Integer.class) diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java index b75cfb681b..017b31129d 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java @@ -15,6 +15,8 @@ */ package org.springframework.data.r2dbc.core; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -25,6 +27,7 @@ import org.springframework.data.r2dbc.mapping.OutboundRow; 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.lang.Nullable; import org.springframework.r2dbc.core.Parameter; import org.springframework.r2dbc.core.PreparedOperation; @@ -82,6 +85,17 @@ public interface ReactiveDataAccessStrategy { */ BiFunction getRowMapper(Class typeToRead); + /** + * Create a flat {@link RowDocument} from a single {@link Readable Row or Stored Procedure output}. + * + * @param type the underlying entity type. + * @param row the row or stored procedure output to retrieve data from. + * @param metadata readable metadata. + * @return the {@link RowDocument} containing the data. + * @since 3.2 + */ + RowDocument toRowDocument(Class type, Readable row, Iterable metadata); + /** * @param type * @return the table name for the {@link Class entity type}. diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java index eb8c332c78..0d1541f324 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveSelectOperationUnitTests.java @@ -28,7 +28,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.data.annotation.Id; import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.testing.StatementRecorder; @@ -103,6 +102,30 @@ void shouldSelectAs() { assertThat(statement.getSql()).isEqualTo("SELECT person.THE_NAME FROM person WHERE person.THE_NAME = $1"); } + @Test // gh-220 + void shouldSelectAsWithColumnName() { + + MockRowMetadata metadata = MockRowMetadata.builder() + .columnMetadata(MockColumnMetadata.builder().name("id").type(R2dbcType.INTEGER).build()) + .columnMetadata(MockColumnMetadata.builder().name("a_different_name").type(R2dbcType.VARCHAR).build()).build(); + MockResult result = MockResult.builder().row(MockRow.builder().identified("id", Object.class, "Walter") + .identified("a_different_name", Object.class, "Werner").metadata(metadata).build()).build(); + + recorder.addStubbing(s -> s.startsWith("SELECT"), result); + + entityTemplate.select(Person.class) // + .as(PersonProjectionWithColumnName.class) // + .matching(query(where("name").is("Walter"))) // + .all() // + .as(StepVerifier::create) // + .assertNext(actual -> assertThat(actual.getName()).isEqualTo("Werner")) // + .verifyComplete(); + + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); + + assertThat(statement.getSql()).isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1"); + } + @Test // gh-220 void shouldSelectFromTable() { @@ -234,6 +257,21 @@ public void setName(String name) { } } + static class PersonProjectionWithColumnName { + + @Id String id; + + @Column("a_different_name") String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + interface PersonProjection { String getName(); From 37fd3c1bb23a71b613e164a3815e204e3d8c312d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Sep 2023 11:54:05 +0200 Subject: [PATCH 4/6] Migrate JDBC to use RowDocument for reading aggregates. --- .../jdbc/core/convert/BasicJdbcConverter.java | 266 ++++++++++++++++-- .../jdbc/core/convert/EntityRowMapper.java | 39 ++- .../data/jdbc/core/convert/Identifier.java | 20 +- .../data/jdbc/core/convert/JdbcConverter.java | 80 ++++-- .../jdbc/core/convert/MapEntityRowMapper.java | 12 +- ...JdbcAggregateTemplateIntegrationTests.java | 101 +++++++ .../conversion/BasicRelationalConverter.java | 2 +- .../MappingRelationalConverter.java | 252 +++++++++++++---- .../core/conversion/ObjectPath.java | 4 +- .../core/conversion/RowDocumentAccessor.java | 4 +- .../data/relational/domain/RowDocument.java | 6 +- 11 files changed, 677 insertions(+), 109 deletions(-) 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 6409390088..c57125233c 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.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.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 BasicJdbcConverter( * @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 @@ private JdbcValue tryToConvertToJdbcValue(@Nullable Object value) { @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 @@ private class ReadingContext { 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 @@ private ReadingContext(AggregatePath rootPath, ResultSetAccessor accessor, Ident 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 @@ private ReadingContext extendBy(RelationalPersistentProperty property) { 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 fe6d0b7a38..1568374623 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 EntityRowMapper(RelationalPersistentEntity entity, JdbcConverter conve } @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 8f7eff1ac0..f1b803bea7 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 static Identifier of(SqlIdentifier name, Object value, Class targetTyp 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 int size() { 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 V get(Object key) { @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 3de2ceb2ec..f7c48304ce 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 @@ default T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, */ 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 @@ default T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, @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 39e70e2ab9..2ec77afe36 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.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 @@ public Map.Entry mapRow(ResultSet rs, int rowNum) throws SQLException 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 e337383fa4..99f505b87e 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 @@ public boolean equals(Object o) { 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 00c435e49a..05ffb29ab1 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 @@ protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation 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 66b42a8d52..30aaac0ce9 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.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 R project(EntityProjection projection, RowDocument document) { 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 @@ private R doReadProjection(ConversionContext context, RowDocument document, 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 @@ String getColumnName(RelationalPersistentProperty prop) { 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 @@ protected S read(TypeInformation type, RowDocument source) @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 @@ protected S readAggregate(ConversionContext context, RowDocum 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 @@ protected Map readMap(ConversionContext context, Map sourc * @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 @@ private T doConvert(Object value, Class target, 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 @@ private S read(ConversionContext context, RelationalPersistentEntity enti 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 @@ private S populateProperties(ConversionContext context, RelationalPersistent 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,54 +502,70 @@ private S populateProperties(ConversionContext context, RelationalPersistent 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 - continue; - } - - if (prop.isEmbedded()) { - accessor.setProperty(prop, readEmbedded(propertyContext, documentAccessor, prop, - getMappingContext().getRequiredPersistentEntity(prop))); + if (property.isEmbedded()) { + accessor.setProperty(property, readEmbedded(propertyContext, valueProviderToUse, documentAccessor, property, + getMappingContext().getRequiredPersistentEntity(property))); continue; } - if (!documentAccessor.hasValue(prop)) { + if (!valueProviderToUse.hasValue(property)) { 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 (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 (prop.findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_EMPTY)) { - return read(context, unwrappedEntity, documentAccessor.getDocument()); + if (onEmpty.equals(OnEmpty.USE_EMPTY)) { + return true; } for (RelationalPersistentProperty persistentProperty : unwrappedEntity) { - if (documentAccessor.hasValue(persistentProperty)) { - return read(context, unwrappedEntity, documentAccessor.getDocument()); + + RelationalPropertyValueProvider contextual = propertyValueProvider + .withContext(context.forProperty(persistentProperty)); + + if (contextual.hasValue(persistentProperty)) { + return true; } } - return null; + return false; } static Predicate isConstructorArgument(PersistentEntity entity) { @@ -532,7 +588,7 @@ protected static class DefaultConversionContext implements ConversionContext { 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 RelationalConverter getSourceConverter() { * * @param */ - interface ValueConverter { + public interface ValueConverter { Object convert(T source, TypeInformation typeHint); @@ -628,7 +684,7 @@ interface ValueConverter { * * @param */ - interface ContainerValueConverter { + public interface ContainerValueConverter { Object convert(ConversionContext context, T source, TypeInformation typeHint); @@ -639,11 +695,11 @@ interface ContainerValueConverter { /** * @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 T getParameterValue(Parameter parame } } + // 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 T getParameterValue(Parameter parame * @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 @@ record RelationalPropertyValueProvider(ConversionContext context, RowDocumentAcc * @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 T getPropertyValue(RelationalPersistentProperty property) { return (T) contextToUse.convert(value, property.getTypeInformation()); } - public RelationalPropertyValueProvider withContext(ConversionContext context) { + @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; + } - return context == this.context ? this - : new RelationalPropertyValueProvider(context, accessor, evaluator, spELContext); + @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 8fc2248e6f..8d0925a56d 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 @@ * @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 4aa62c7694..5d991c1027 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 @@ * @author Mark Paluch * @since 3.2 */ -class RowDocumentAccessor { +public class RowDocumentAccessor { private final RowDocument document; @@ -103,7 +103,7 @@ public boolean hasValue(RelationalPersistentProperty property) { 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 38d77b2adf..7c69413f24 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 RowDocument() { 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); } /** From 55c3894c8578a36791621253250f2573762edeca Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Sep 2023 16:07:08 +0200 Subject: [PATCH 5/6] =?UTF-8?q?Deprecate=20Basic=E2=80=A6Converter=20infra?= =?UTF-8?q?structure.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge basic converters into Mapping…Converters and introduce deprecated variant to provide guidance for migration off the deprecated types. Cleanup no longer required code. --- .../JdbcAggregateChangeExecutionContext.java | 15 +- .../jdbc/core/convert/BasicJdbcConverter.java | 796 +----------------- .../jdbc/core/convert/EntityRowMapper.java | 42 +- .../data/jdbc/core/convert/JdbcConverter.java | 49 +- .../jdbc/core/convert/MapEntityRowMapper.java | 11 +- .../core/convert/MappingJdbcConverter.java | 537 ++++++++++++ .../RowDocumentResultSetExtractor.java | 22 + .../config/AbstractJdbcConfiguration.java | 3 +- .../AggregateChangeIdGenerationUnitTests.java | 6 +- ...angeExecutorContextImmutableUnitTests.java | 37 +- ...gregateChangeExecutorContextUnitTests.java | 4 +- .../core/JdbcAggregateTemplateUnitTests.java | 16 +- ...lConverterAggregateReferenceUnitTests.java | 5 +- .../DefaultDataAccessStrategyUnitTests.java | 12 +- .../convert/EntityRowMapperUnitTests.java | 104 +-- .../JdbcIdentifierBuilderUnitTests.java | 2 +- ...ava => MappingJdbcConverterUnitTests.java} | 6 +- .../core/convert/QueryMapperUnitTests.java | 3 +- ...orContextBasedNamingStrategyUnitTests.java | 3 +- .../SqlGeneratorEmbeddedUnitTests.java | 2 +- ...GeneratorFixedNamingStrategyUnitTests.java | 3 +- .../core/convert/SqlGeneratorUnitTests.java | 2 +- .../convert/SqlParametersFactoryTest.java | 12 +- ...tomizingNamespaceHsqlIntegrationTests.java | 4 +- .../SimpleJdbcRepositoryEventsUnitTests.java | 15 +- .../query/PartTreeJdbcQueryUnitTests.java | 4 +- .../query/StringBasedJdbcQueryUnitTests.java | 17 +- .../JdbcRepositoryFactoryBeanUnitTests.java | 6 +- .../data/jdbc/testing/TestConfiguration.java | 5 +- .../r2dbc/convert/MappingR2dbcConverter.java | 299 +------ .../data/r2dbc/convert/R2dbcConverter.java | 9 - .../AbstractRelationalConverter.java | 98 +++ .../conversion/BasicRelationalConverter.java | 297 +------ .../MappingRelationalConverter.java | 265 +++++- .../core/conversion/RelationalConverter.java | 71 +- 35 files changed, 1150 insertions(+), 1632 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/{BasicJdbcConverterUnitTests.java => MappingJdbcConverterUnitTests.java} (98%) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AbstractRelationalConverter.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java index ed67dfdfb4..8c37babdd0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java @@ -15,7 +15,16 @@ */ package org.springframework.data.jdbc.core; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -33,7 +42,6 @@ import org.springframework.data.relational.core.conversion.DbActionExecutionResult; import org.springframework.data.relational.core.conversion.IdValueSource; 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; @@ -188,7 +196,8 @@ private Identifier getParentKeys(DbAction.WithDependingOn action, JdbcConvert private Object getParentId(DbAction.WithDependingOn action) { - DbAction.WithEntity idOwningAction = getIdOwningAction(action, context.getAggregatePath(action.getPropertyPath()).getIdDefiningParentPath()); + DbAction.WithEntity idOwningAction = getIdOwningAction(action, + context.getAggregatePath(action.getPropertyPath()).getIdDefiningParentPath()); return getPotentialGeneratedIdFrom(idOwningAction); } 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 c57125233c..32d66d0b9d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * Copyright 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. @@ -15,78 +15,25 @@ */ package org.springframework.data.jdbc.core.convert; -import java.sql.Array; -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; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.core.convert.ConverterNotFoundException; -import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.CustomConversions; -import org.springframework.data.jdbc.core.mapping.AggregateReference; -import org.springframework.data.jdbc.core.mapping.JdbcValue; -import org.springframework.data.jdbc.support.JdbcUtil; -import org.springframework.data.mapping.InstanceCreatorMetadata; -import org.springframework.data.mapping.Parameter; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; -import org.springframework.data.mapping.model.ParameterValueProvider; -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; /** - * {@link RelationalConverter} that uses a {@link MappingContext} to apply basic conversion of relational values to - * property values. + * {@link RelationalConverter} that uses a {@link MappingContext} to apply conversion of relational values to property + * values. *

* Conversion is configurable by providing a customized {@link CustomConversions}. * * @author Mark Paluch - * @author Jens Schauder - * @author Christoph Strobl - * @author Myeonghyeon Lee - * @author Chirag Tailor - * @see MappingContext - * @see SimpleTypeHolder - * @see CustomConversions * @since 1.1 + * @deprecated since 3.2, use {@link MappingJdbcConverter} instead as the naming suggests a limited scope of + * functionality. */ -public class BasicJdbcConverter extends MappingRelationalConverter implements JdbcConverter, ApplicationContextAware { - - private static final Log LOG = LogFactory.getLog(BasicJdbcConverter.class); - private static final Converter, Map> ITERABLE_OF_ENTRY_TO_MAP_CONVERTER = new IterableOfEntryToMapConverter(); - - private final JdbcTypeFactory typeFactory; - private final IdentifierProcessing identifierProcessing; - - private final RelationResolver relationResolver; - private SpELContext spELContext; +@Deprecated(since = "3.2") +public class BasicJdbcConverter extends MappingJdbcConverter { /** * Creates a new {@link BasicJdbcConverter} given {@link MappingContext} and a {@link JdbcTypeFactory#unsupported() @@ -98,15 +45,7 @@ public class BasicJdbcConverter extends MappingRelationalConverter implements Jd * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. */ public BasicJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) { - - super(context, new JdbcCustomConversions()); - - Assert.notNull(relationResolver, "RelationResolver must not be null"); - - this.typeFactory = JdbcTypeFactory.unsupported(); - this.identifierProcessing = IdentifierProcessing.ANSI; - this.relationResolver = relationResolver; - this.spELContext = new SpELContext(ResultSetAccessorPropertyAccessor.INSTANCE); + super(context, relationResolver); } /** @@ -115,719 +54,24 @@ public BasicJdbcConverter(RelationalMappingContext context, RelationResolver rel * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. * @param typeFactory must not be {@literal null} - * @param identifierProcessing must not be {@literal null} - * @since 2.0 + * @since 3.2 */ public BasicJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, - CustomConversions conversions, JdbcTypeFactory typeFactory, IdentifierProcessing identifierProcessing) { - - super(context, conversions); - - Assert.notNull(typeFactory, "JdbcTypeFactory must not be null"); - Assert.notNull(relationResolver, "RelationResolver must not be null"); - Assert.notNull(identifierProcessing, "IdentifierProcessing must not be null"); - - this.typeFactory = typeFactory; - this.identifierProcessing = identifierProcessing; - this.relationResolver = relationResolver; - this.spELContext = new SpELContext(ResultSetAccessorPropertyAccessor.INSTANCE); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) { - this.spELContext = new SpELContext(this.spELContext, applicationContext); - } - - @Nullable - private Class getEntityColumnType(Class type) { - - RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type); - - if (persistentEntity == null) { - return null; - } - - RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); - - if (idProperty == null) { - return null; - } - return getColumnType(idProperty); - } - - private Class getReferenceColumnType(RelationalPersistentProperty property) { - - Class componentType = property.getTypeInformation().getRequiredComponentType().getType(); - RelationalPersistentEntity referencedEntity = getMappingContext().getRequiredPersistentEntity(componentType); - - return getColumnType(referencedEntity.getRequiredIdProperty()); - } - - @Override - public SQLType getTargetSqlType(RelationalPersistentProperty property) { - return JdbcUtil.targetSqlTypeFor(getColumnType(property)); - } - - @Override - public Class getColumnType(RelationalPersistentProperty property) { - return doGetColumnType(property); - } - - private Class doGetColumnType(RelationalPersistentProperty property) { - - if (property.isAssociation()) { - return getReferenceColumnType(property); - } - - if (property.isEntity()) { - Class columnType = getEntityColumnType(property.getActualType()); - - if (columnType != null) { - return columnType; - } - } - - Class componentColumnType = JdbcColumnTypes.INSTANCE.resolvePrimitiveType(property.getActualType()); - - while (componentColumnType.isArray()) { - componentColumnType = componentColumnType.getComponentType(); - } - - if (property.isCollectionLike() && !property.isEntity()) { - return java.lang.reflect.Array.newInstance(componentColumnType, 0).getClass(); - } - - return componentColumnType; - } - - @Override - @Nullable - public Object readValue(@Nullable Object value, TypeInformation type) { - - if (value == null) { - return value; - } - - if (value instanceof Array) { - try { - return super.readValue(((Array) value).getArray(), type); - } catch (SQLException | ConverterNotFoundException e) { - LOG.info("Failed to extract a value of type %s from an Array; Attempting to use standard conversions", e); - } - } - - return super.readValue(value, type); - } - - @Override - @Nullable - public Object writeValue(@Nullable Object value, TypeInformation type) { - - if (value == null) { - return null; - } - - return super.writeValue(value, type); - } - - private boolean canWriteAsJdbcValue(@Nullable Object value) { - - if (value == null) { - return true; - } - - if (AggregateReference.class.isAssignableFrom(value.getClass())) { - return canWriteAsJdbcValue(((AggregateReference) value).getId()); - } - - RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(value.getClass()); - - if (persistentEntity != null) { - - Object id = persistentEntity.getIdentifierAccessor(value).getIdentifier(); - return canWriteAsJdbcValue(id); - } - - if (value instanceof JdbcValue) { - return true; - } - - Optional> customWriteTarget = getConversions().getCustomWriteTarget(value.getClass()); - return customWriteTarget.isPresent() && customWriteTarget.get().isAssignableFrom(JdbcValue.class); - } - - @Override - public JdbcValue writeJdbcValue(@Nullable Object value, Class columnType, SQLType sqlType) { - - JdbcValue jdbcValue = tryToConvertToJdbcValue(value); - if (jdbcValue != null) { - return jdbcValue; - } - - Object convertedValue = writeValue(value, TypeInformation.of(columnType)); - - if (convertedValue == null || !convertedValue.getClass().isArray()) { - - return JdbcValue.of(convertedValue, sqlType); - } - - Class componentType = convertedValue.getClass().getComponentType(); - if (componentType != byte.class && componentType != Byte.class) { - - Object[] objectArray = requireObjectArray(convertedValue); - return JdbcValue.of(typeFactory.createArray(objectArray), JDBCType.ARRAY); - } - - if (componentType == Byte.class) { - convertedValue = ArrayUtils.toPrimitive((Byte[]) convertedValue); - } - - return JdbcValue.of(convertedValue, JDBCType.BINARY); - } - - @Nullable - private JdbcValue tryToConvertToJdbcValue(@Nullable Object value) { - - if (canWriteAsJdbcValue(value)) { - - Object converted = writeValue(value, TypeInformation.of(JdbcValue.class)); - if (converted instanceof JdbcValue) { - return (JdbcValue) converted; - } - } - - return null; - } - - @Override - public T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key) { - 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); + CustomConversions conversions, JdbcTypeFactory typeFactory) { + super(context, relationResolver, conversions, typeFactory); } /** - * {@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. + * Creates a new {@link BasicJdbcConverter} given {@link MappingContext}. * - * @param delegate - * @param aggregatePath - * @param identifier + * @param context must not be {@literal null}. + * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. + * @param typeFactory must not be {@literal null} + * @param identifierProcessing must not be {@literal null} + * @since 2.0 */ - 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"); - - Class componentType = source.getClass().getComponentType(); - - if (componentType.isPrimitive()) { - if (componentType == boolean.class) { - return ArrayUtils.toObject((boolean[]) source); - } - if (componentType == byte.class) { - return ArrayUtils.toObject((byte[]) source); - } - if (componentType == char.class) { - return ArrayUtils.toObject((char[]) source); - } - if (componentType == double.class) { - return ArrayUtils.toObject((double[]) source); - } - if (componentType == float.class) { - return ArrayUtils.toObject((float[]) source); - } - if (componentType == int.class) { - return ArrayUtils.toObject((int[]) source); - } - if (componentType == long.class) { - return ArrayUtils.toObject((long[]) source); - } - if (componentType == short.class) { - return ArrayUtils.toObject((short[]) source); - } - - throw new IllegalArgumentException("Unsupported component type: " + componentType); - } - return (Object[]) source; - } - - private class ReadingContext { - - private final RelationalPersistentEntity entity; - - private final AggregatePath rootPath; - private final AggregatePath path; - private final Identifier identifier; - private final Object key; - - private final JdbcPropertyValueProvider propertyValueProvider; - private final JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider; - private final ResultSetAccessor accessor; - - @SuppressWarnings("unchecked") - 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.identifier = identifier; - this.key = key; - this.propertyValueProvider = new JdbcPropertyValueProvider(path, accessor); - this.backReferencePropertyValueProvider = new JdbcBackReferencePropertyValueProvider(path, accessor); - this.accessor = accessor; - } - - private ReadingContext(RelationalPersistentEntity entity, AggregatePath rootPath, AggregatePath path, - Identifier identifier, Object key, JdbcPropertyValueProvider propertyValueProvider, - JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider, ResultSetAccessor accessor) { - - this.entity = entity; - this.rootPath = rootPath; - this.path = path; - this.identifier = identifier; - this.key = key; - this.propertyValueProvider = propertyValueProvider; - this.backReferencePropertyValueProvider = backReferencePropertyValueProvider; - this.accessor = accessor; - } - - private ReadingContext extendBy(RelationalPersistentProperty property) { - - return new ReadingContext<>( - (RelationalPersistentEntity) getMappingContext().getRequiredPersistentEntity(property.getActualType()), - rootPath.append(property), path.append(property), identifier, key, propertyValueProvider.extendBy(property), - backReferencePropertyValueProvider.extendBy(property), accessor); - } - - T mapRow() { - - RelationalPersistentProperty idProperty = entity.getIdProperty(); - - Object idValue = idProperty == null ? null : readFrom(idProperty); - - return createInstanceInternal(idValue); - } - - private T populateProperties(T instance, @Nullable Object idValue) { - - PersistentPropertyAccessor propertyAccessor = getPropertyAccessor(entity, instance); - InstanceCreatorMetadata creatorMetadata = entity.getInstanceCreatorMetadata(); - - entity.doWithAll(property -> { - - if (creatorMetadata != null && creatorMetadata.isCreatorParameter(property)) { - return; - } - - // skip absent simple properties - if (isSimpleProperty(property)) { - - if (!propertyValueProvider.hasProperty(property)) { - return; - } - } - - Object value = readOrLoadProperty(idValue, property); - propertyAccessor.setProperty(property, value); - }); - - return propertyAccessor.getBean(); - } - - @Nullable - private Object readOrLoadProperty(@Nullable Object id, RelationalPersistentProperty property) { - - if ((property.isCollectionLike() && property.isEntity()) || property.isMap()) { - - Iterable allByPath = resolveRelation(id, property); - - return property.isMap() // - ? ITERABLE_OF_ENTRY_TO_MAP_CONVERTER.convert(allByPath) // - : allByPath; - - } else if (property.isEmbedded()) { - return readEmbeddedEntityFrom(id, property); - } else { - return readFrom(property); - } - } - - private Iterable resolveRelation(@Nullable Object id, RelationalPersistentProperty property) { - - Identifier identifier = id == null // - ? this.identifier.withPart(rootPath.getTableInfo().qualifierColumnInfo().name(), key, Object.class) // - : Identifier.of(rootPath.append(property).getTableInfo().reverseColumnInfo().name(), id, Object.class); - - PersistentPropertyPath propertyPath = path.append(property) - .getRequiredPersistentPropertyPath(); - - return relationResolver.findAllByPath(identifier, propertyPath); - } - - /** - * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument. - * - * @param property the {@link RelationalPersistentProperty} for which the value is intended. Must not be - * {@code null}. - * @return the value read from the {@link ResultSet}. May be {@code null}. - */ - @Nullable - private Object readFrom(RelationalPersistentProperty property) { - - if (property.isEntity()) { - return readEntityFrom(property); - } - - Object value = propertyValueProvider.getPropertyValue(property); - return value != null ? readValue(value, property.getTypeInformation()) : null; - } - - @Nullable - private Object readEmbeddedEntityFrom(@Nullable Object idValue, RelationalPersistentProperty property) { - - ReadingContext newContext = extendBy(property); - - if (shouldCreateEmptyEmbeddedInstance(property) || newContext.hasInstanceValues(idValue)) { - return newContext.createInstanceInternal(idValue); - } - - return null; - } - - private boolean shouldCreateEmptyEmbeddedInstance(RelationalPersistentProperty property) { - return property.shouldCreateEmptyEmbedded(); - } - - private boolean hasInstanceValues(@Nullable Object idValue) { - - RelationalPersistentEntity persistentEntity = path.getRequiredLeafEntity(); - - for (RelationalPersistentProperty embeddedProperty : persistentEntity) { - - // if the embedded contains Lists, Sets or Maps we consider it non-empty - if (embeddedProperty.isQualified() || embeddedProperty.isAssociation()) { - return true; - } - - Object value = readOrLoadProperty(idValue, embeddedProperty); - if (value != null) { - return true; - } - } - - return false; - } - - @Nullable - @SuppressWarnings("unchecked") - private Object readEntityFrom(RelationalPersistentProperty property) { - - ReadingContext newContext = extendBy(property); - RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(property.getActualType()); - RelationalPersistentProperty idProperty = entity.getIdProperty(); - - Object idValue; - - if (idProperty != null) { - idValue = newContext.readFrom(idProperty); - } else { - idValue = backReferencePropertyValueProvider.getPropertyValue(property); - } - - if (idValue == null) { - return null; - } - - return newContext.createInstanceInternal(idValue); - } - - private T createInstanceInternal(@Nullable Object idValue) { - - InstanceCreatorMetadata creatorMetadata = entity.getInstanceCreatorMetadata(); - ParameterValueProvider provider; - - if (creatorMetadata != null && creatorMetadata.hasParameters()) { - - SpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(accessor, spELContext); - provider = new SpELExpressionParameterValueProvider<>(expressionEvaluator, getConversionService(), - new ResultSetParameterValueProvider(idValue, entity)); - } else { - provider = NoOpParameterValueProvider.INSTANCE; - } - - T instance = createInstance(entity, provider::getParameterValue); - - return entity.requiresPropertyPopulation() ? populateProperties(instance, idValue) : instance; - } - - /** - * {@link ParameterValueProvider} that reads a simple property or materializes an object for a - * {@link RelationalPersistentProperty}. - * - * @see #readOrLoadProperty(Object, RelationalPersistentProperty) - * @since 2.1 - */ - private class ResultSetParameterValueProvider implements ParameterValueProvider { - - private final @Nullable Object idValue; - private final RelationalPersistentEntity entity; - - public ResultSetParameterValueProvider(@Nullable Object idValue, RelationalPersistentEntity entity) { - this.idValue = idValue; - this.entity = entity; - } - - @Override - @Nullable - public T getParameterValue(Parameter parameter) { - - String parameterName = parameter.getName(); - - Assert.notNull(parameterName, "A constructor parameter name must not be null to be used with Spring Data JDBC"); - - RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameterName); - return (T) readOrLoadProperty(idValue, property); - } - } - } - - private boolean isSimpleProperty(RelationalPersistentProperty property) { - return !property.isCollectionLike() && !property.isEntity() && !property.isMap() && !property.isEmbedded(); - } - - enum NoOpParameterValueProvider implements ParameterValueProvider { - - INSTANCE; - - @Override - public T getParameterValue(Parameter parameter) { - return null; - } + public BasicJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, + CustomConversions conversions, JdbcTypeFactory typeFactory, IdentifierProcessing identifierProcessing) { + super(context, relationResolver, conversions, typeFactory, identifierProcessing); } - } 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 1568374623..b2e960b0ab 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,9 +15,7 @@ */ 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; @@ -25,7 +23,7 @@ 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; +import org.springframework.lang.Nullable; /** * Maps a {@link ResultSet} to an entity of type {@code T}, including entities referenced. This {@link RowMapper} might @@ -43,7 +41,7 @@ public class EntityRowMapper implements RowMapper { private final RelationalPersistentEntity entity; private final AggregatePath path; private final JdbcConverter converter; - private final Identifier identifier; + private final @Nullable Identifier identifier; /** * @deprecated use {@link EntityRowMapper#EntityRowMapper(AggregatePath, JdbcConverter, Identifier)} instead @@ -78,39 +76,11 @@ public EntityRowMapper(RelationalPersistentEntity entity, JdbcConverter conve @Override public T mapRow(ResultSet resultSet, int rowNumber) throws SQLException { - RowDocument document = toRowDocument(resultSet); + RowDocument document = RowDocumentResultSetExtractor.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); + return identifier == null // + ? converter.readAndResolve(entity.getType(), document) // + : converter.readAndResolve(entity.getType(), document, identifier); } - /** - * 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/JdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java index f7c48304ce..6213c4ed96 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 @@ -16,12 +16,11 @@ package org.springframework.data.jdbc.core.convert; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.SQLType; import org.springframework.data.jdbc.core.mapping.JdbcValue; -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; @@ -58,8 +57,16 @@ public interface JdbcConverter extends RelationalConverter { * @param key primary key. * @param * @return + * @deprecated since 3.2, use {@link #readAndResolve(Class, RowDocument, Identifier)} instead. */ - T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key); + @Deprecated(since = "3.2") + default T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key) { + try { + return readAndResolve(entity.getType(), RowDocumentResultSetExtractor.toRowDocument(resultSet)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } /** * Read the current row from {@link ResultSet} to an {@link PersistentPropertyPathExtension#getActualType() entity}. @@ -70,39 +77,19 @@ public interface JdbcConverter extends RelationalConverter { * @param key primary key. * @param * @return - * @deprecated use {@link #mapRow(AggregatePath, ResultSet, Identifier, Object)} instead. + * @deprecated use {@link #readAndResolve(Class, RowDocument, Identifier)} instead. */ + @SuppressWarnings("unchecked") @Deprecated(since = "3.2", forRemoval = true) default T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key) { - return mapRow(path.getAggregatePath(), resultSet, identifier, key); + try { + return (T) readAndResolve(path.getRequiredLeafEntity().getType(), + RowDocumentResultSetExtractor.toRowDocument(resultSet), identifier); + } catch (SQLException e) { + throw new RuntimeException(e); + } }; - /** - * Read the current row from {@link ResultSet} to an {@link AggregatePath#getLeafEntity()} entity}. - * - * @param path path to the owning property. - * @param resultSet the {@link ResultSet} to read from. - * @param identifier entity identifier. - * @param key primary key. - * @param - * @return - */ - 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}. 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 2ec77afe36..05430670eb 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 @@ -55,14 +55,11 @@ public Map.Entry mapRow(ResultSet rs, int rowNum) throws SQLException return new HashMap.SimpleEntry<>(key, mapEntity(rs, key)); } + @SuppressWarnings("unchecked") 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); + RowDocument document = RowDocumentResultSetExtractor.toRowDocument(resultSet); + return (T) converter.readAndResolve(path.getLeafEntity().getType(), document, + identifier.withPart(keyColumn, key, Object.class)); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java new file mode 100644 index 0000000000..90c7bfdafe --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java @@ -0,0 +1,537 @@ +/* + * Copyright 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.Array; +import java.sql.JDBCType; +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; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.data.jdbc.core.mapping.JdbcValue; +import org.springframework.data.jdbc.support.JdbcUtil; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.mapping.model.SpELExpressionEvaluator; +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; + +/** + * {@link RelationalConverter} that uses a {@link MappingContext} to apply conversion of relational values to property + * values. + *

+ * Conversion is configurable by providing a customized {@link CustomConversions}. + * + * @author Mark Paluch + * @author Jens Schauder + * @author Christoph Strobl + * @author Myeonghyeon Lee + * @author Chirag Tailor + * @see MappingContext + * @see SimpleTypeHolder + * @see CustomConversions + * @since 3.2 + */ +public class MappingJdbcConverter extends MappingRelationalConverter implements JdbcConverter, ApplicationContextAware { + + private static final Log LOG = LogFactory.getLog(MappingJdbcConverter.class); + private static final Converter, Map> ITERABLE_OF_ENTRY_TO_MAP_CONVERTER = new IterableOfEntryToMapConverter(); + + private final JdbcTypeFactory typeFactory; + private final RelationResolver relationResolver; + + /** + * Creates a new {@link MappingJdbcConverter} given {@link MappingContext} and a {@link JdbcTypeFactory#unsupported() + * no-op type factory} throwing {@link UnsupportedOperationException} on type creation. Use + * {@link #MappingJdbcConverter(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 MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) { + + super(context, new JdbcCustomConversions()); + + Assert.notNull(relationResolver, "RelationResolver must not be null"); + + this.typeFactory = JdbcTypeFactory.unsupported(); + this.relationResolver = relationResolver; + } + + /** + * Creates a new {@link MappingJdbcConverter} given {@link MappingContext}. + * + * @param context must not be {@literal null}. + * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. + * @param typeFactory must not be {@literal null} + */ + public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, + CustomConversions conversions, JdbcTypeFactory typeFactory) { + + super(context, conversions); + + Assert.notNull(typeFactory, "JdbcTypeFactory must not be null"); + Assert.notNull(relationResolver, "RelationResolver must not be null"); + + this.typeFactory = typeFactory; + this.relationResolver = relationResolver; + } + + MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, + CustomConversions conversions, JdbcTypeFactory typeFactory, IdentifierProcessing identifierProcessing) { + super(context, conversions); + this.relationResolver = relationResolver; + this.typeFactory = typeFactory; + } + + @Nullable + private Class getEntityColumnType(Class type) { + + RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type); + + if (persistentEntity == null) { + return null; + } + + RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); + + if (idProperty == null) { + return null; + } + return getColumnType(idProperty); + } + + private Class getReferenceColumnType(RelationalPersistentProperty property) { + + Class componentType = property.getTypeInformation().getRequiredComponentType().getType(); + RelationalPersistentEntity referencedEntity = getMappingContext().getRequiredPersistentEntity(componentType); + + return getColumnType(referencedEntity.getRequiredIdProperty()); + } + + @Override + public SQLType getTargetSqlType(RelationalPersistentProperty property) { + return JdbcUtil.targetSqlTypeFor(getColumnType(property)); + } + + @Override + public Class getColumnType(RelationalPersistentProperty property) { + return doGetColumnType(property); + } + + private Class doGetColumnType(RelationalPersistentProperty property) { + + if (property.isAssociation()) { + return getReferenceColumnType(property); + } + + if (property.isEntity()) { + Class columnType = getEntityColumnType(property.getActualType()); + + if (columnType != null) { + return columnType; + } + } + + Class componentColumnType = JdbcColumnTypes.INSTANCE.resolvePrimitiveType(property.getActualType()); + + while (componentColumnType.isArray()) { + componentColumnType = componentColumnType.getComponentType(); + } + + if (property.isCollectionLike() && !property.isEntity()) { + return java.lang.reflect.Array.newInstance(componentColumnType, 0).getClass(); + } + + return componentColumnType; + } + + @Override + @Nullable + public Object readValue(@Nullable Object value, TypeInformation type) { + + if (value == null) { + return value; + } + + if (value instanceof Array) { + try { + return super.readValue(((Array) value).getArray(), type); + } catch (SQLException | ConverterNotFoundException e) { + LOG.info("Failed to extract a value of type %s from an Array; Attempting to use standard conversions", e); + } + } + + return super.readValue(value, type); + } + + @Override + @Nullable + public Object writeValue(@Nullable Object value, TypeInformation type) { + + if (value == null) { + return null; + } + + return super.writeValue(value, type); + } + + private boolean canWriteAsJdbcValue(@Nullable Object value) { + + if (value == null) { + return true; + } + + if (AggregateReference.class.isAssignableFrom(value.getClass())) { + return canWriteAsJdbcValue(((AggregateReference) value).getId()); + } + + RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(value.getClass()); + + if (persistentEntity != null) { + + Object id = persistentEntity.getIdentifierAccessor(value).getIdentifier(); + return canWriteAsJdbcValue(id); + } + + if (value instanceof JdbcValue) { + return true; + } + + Optional> customWriteTarget = getConversions().getCustomWriteTarget(value.getClass()); + return customWriteTarget.isPresent() && customWriteTarget.get().isAssignableFrom(JdbcValue.class); + } + + @Override + public JdbcValue writeJdbcValue(@Nullable Object value, Class columnType, SQLType sqlType) { + + JdbcValue jdbcValue = tryToConvertToJdbcValue(value); + if (jdbcValue != null) { + return jdbcValue; + } + + Object convertedValue = writeValue(value, TypeInformation.of(columnType)); + + if (convertedValue == null || !convertedValue.getClass().isArray()) { + + return JdbcValue.of(convertedValue, sqlType); + } + + Class componentType = convertedValue.getClass().getComponentType(); + if (componentType != byte.class && componentType != Byte.class) { + + Object[] objectArray = requireObjectArray(convertedValue); + return JdbcValue.of(typeFactory.createArray(objectArray), JDBCType.ARRAY); + } + + if (componentType == Byte.class) { + convertedValue = ArrayUtils.toPrimitive((Byte[]) convertedValue); + } + + return JdbcValue.of(convertedValue, JDBCType.BINARY); + } + + @Nullable + private JdbcValue tryToConvertToJdbcValue(@Nullable Object value) { + + if (canWriteAsJdbcValue(value)) { + + Object converted = writeValue(value, TypeInformation.of(JdbcValue.class)); + if (converted instanceof JdbcValue) { + return (JdbcValue) converted; + } + } + + return null; + } + + @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 identifierToUse = this.identifier; + + if (property.getOwner().hasIdProperty()) { + + Object id = this.identifier.get(property.getOwner().getRequiredIdProperty().getColumnName()); + + if (id != null) { + identifierToUse = Identifier.of(aggregatePath.getTableInfo().reverseColumnInfo().name(), id, + Object.class); + } + } + + Iterable allByPath = relationResolver.findAllByPath(identifierToUse, + 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"); + + Class componentType = source.getClass().getComponentType(); + + if (componentType.isPrimitive()) { + if (componentType == boolean.class) { + return ArrayUtils.toObject((boolean[]) source); + } + if (componentType == byte.class) { + return ArrayUtils.toObject((byte[]) source); + } + if (componentType == char.class) { + return ArrayUtils.toObject((char[]) source); + } + if (componentType == double.class) { + return ArrayUtils.toObject((double[]) source); + } + if (componentType == float.class) { + return ArrayUtils.toObject((float[]) source); + } + if (componentType == int.class) { + return ArrayUtils.toObject((int[]) source); + } + if (componentType == long.class) { + return ArrayUtils.toObject((long[]) source); + } + if (componentType == short.class) { + return ArrayUtils.toObject((short[]) source); + } + + throw new IllegalArgumentException("Unsupported component type: " + componentType); + } + return (Object[]) source; + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentResultSetExtractor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentResultSetExtractor.java index 45b264050d..00b1010113 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentResultSetExtractor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentResultSetExtractor.java @@ -52,6 +52,28 @@ class RowDocumentResultSetExtractor { this.propertyToColumn = propertyToColumn; } + /** + * 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; + } + /** * Adapter to extract values and column metadata from a {@link ResultSet}. */ diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index 9466675079..197bb08f66 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -135,8 +135,7 @@ public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext, NamedParam : JdbcArrayColumns.DefaultSupport.INSTANCE; DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations(), arrayColumns); - return new BasicJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory, - dialect.getIdentifierProcessing()); + return new MappingJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory); } /** diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java index 75a2e4d426..710746997c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java @@ -31,15 +31,15 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; -import org.springframework.data.relational.core.conversion.RootAggregateChange; import org.springframework.data.relational.core.conversion.DbAction; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.conversion.MutableAggregateChange; +import org.springframework.data.relational.core.conversion.RootAggregateChange; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.lang.Nullable; @@ -61,7 +61,7 @@ public class AggregateChangeIdGenerationUnitTests { Tag tag3 = new Tag(); RelationalMappingContext context = new RelationalMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); DbAction.WithRoot rootInsert = new DbAction.InsertRoot<>(entity, IdValueSource.GENERATED); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java index 88f644852f..d56f323058 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java @@ -15,14 +15,21 @@ */ package org.springframework.data.jdbc.core; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Objects; + import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.Identifier; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; +import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction; @@ -32,12 +39,6 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.lang.Nullable; -import java.util.List; - -import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - /** * Test for the {@link JdbcAggregateChangeExecutionContext} when operating on immutable classes. * @@ -47,7 +48,7 @@ public class JdbcAggregateChangeExecutorContextImmutableUnitTests { RelationalMappingContext context = new RelationalMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); @@ -221,19 +222,20 @@ public List getList() { public boolean equals(final Object o) { if (o == this) return true; - if (!(o instanceof DummyEntity)) return false; - final DummyEntity other = (DummyEntity) o; + if (!(o instanceof final DummyEntity other)) + return false; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false; + if (!Objects.equals(this$id, other$id)) + return false; if (this.getVersion() != other.getVersion()) return false; final Object this$content = this.getContent(); final Object other$content = other.getContent(); - if (this$content == null ? other$content != null : !this$content.equals(other$content)) return false; + if (!Objects.equals(this$content, other$content)) + return false; final Object this$list = this.getList(); final Object other$list = other.getList(); - if (this$list == null ? other$list != null : !this$list.equals(other$list)) return false; - return true; + return Objects.equals(this$list, other$list); } public int hashCode() { @@ -289,12 +291,11 @@ public Long getId() { public boolean equals(final Object o) { if (o == this) return true; - if (!(o instanceof Content)) return false; - final Content other = (Content) o; + if (!(o instanceof final Content other)) + return false; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false; - return true; + return Objects.equals(this$id, other$id); } public int hashCode() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java index 1a8df7c788..aeb9f3fd7c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java @@ -25,11 +25,11 @@ import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.Identifier; import org.springframework.data.jdbc.core.convert.InsertSubject; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction; @@ -50,7 +50,7 @@ public class JdbcAggregateChangeExecutorContextUnitTests { RelationalMappingContext context = new RelationalMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateUnitTests.java index d95c3e0cd7..90c186b947 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateUnitTests.java @@ -15,6 +15,11 @@ */ package org.springframework.data.jdbc.core; +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,9 +31,9 @@ import org.springframework.data.annotation.Version; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.conversion.MutableAggregateChange; @@ -41,11 +46,6 @@ import org.springframework.data.relational.core.mapping.event.BeforeDeleteCallback; import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback; -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - /** * Unit tests for {@link JdbcAggregateTemplate}. * @@ -72,10 +72,10 @@ public class JdbcAggregateTemplateUnitTests { public void setUp() { RelationalMappingContext mappingContext = new RelationalMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(mappingContext, relationResolver); + JdbcConverter converter = new MappingJdbcConverter(mappingContext, relationResolver); template = new JdbcAggregateTemplate(eventPublisher, mappingContext, converter, dataAccessStrategy); - ((JdbcAggregateTemplate) template).setEntityCallbacks(callbacks); + template.setEntityCallbacks(callbacks); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java index a53b773a95..68c9611e1e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java @@ -28,15 +28,14 @@ import org.springframework.data.util.TypeInformation; /** - * Unit tests for the handling of {@link AggregateReference}s in the - * {@link org.springframework.data.jdbc.core.convert.BasicJdbcConverter}. + * Unit tests for the handling of {@link AggregateReference}s in the {@link MappingJdbcConverter}. * * @author Jens Schauder */ public class BasicRelationalConverterAggregateReferenceUnitTests { JdbcMappingContext context = new JdbcMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); + JdbcConverter converter = new MappingJdbcConverter(context, mock(RelationResolver.class)); RelationalPersistentEntity entity = context.getRequiredPersistentEntity(DummyEntity.class); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java index 076e877f50..e97df419ec 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java @@ -45,11 +45,11 @@ class DefaultDataAccessStrategyUnitTests { static final long ORIGINAL_ID = 4711L; - private NamedParameterJdbcOperations namedJdbcOperations = mock(NamedParameterJdbcOperations.class); - private JdbcOperations jdbcOperations = mock(JdbcOperations.class); - private RelationalMappingContext context = new JdbcMappingContext(); - private SqlParametersFactory sqlParametersFactory = mock(SqlParametersFactory.class); - private InsertStrategyFactory insertStrategyFactory = mock(InsertStrategyFactory.class); + private final NamedParameterJdbcOperations namedJdbcOperations = mock(NamedParameterJdbcOperations.class); + private final JdbcOperations jdbcOperations = mock(JdbcOperations.class); + private final RelationalMappingContext context = new JdbcMappingContext(); + private final SqlParametersFactory sqlParametersFactory = mock(SqlParametersFactory.class); + private final InsertStrategyFactory insertStrategyFactory = mock(InsertStrategyFactory.class); private JdbcConverter converter; private DataAccessStrategy accessStrategy; @@ -59,7 +59,7 @@ void before() { DelegatingDataAccessStrategy relationResolver = new DelegatingDataAccessStrategy(); Dialect dialect = HsqlDbDialect.INSTANCE; - converter = new BasicJdbcConverter(context, relationResolver, new JdbcCustomConversions(), + converter = new MappingJdbcConverter(context, relationResolver, new JdbcCustomConversions(), new DefaultJdbcTypeFactory(jdbcOperations), dialect.getIdentifierProcessing()); accessStrategy = new DataAccessStrategyFactory( // new SqlGeneratorSource(context, converter, dialect), // diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java index c2cba6c5a5..454b28c3a2 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java @@ -15,6 +15,28 @@ */ package org.springframework.data.jdbc.core.convert; +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.SoftAssertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.naming.OperationNotSupportedException; + import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.mockito.invocation.InvocationOnMock; @@ -37,26 +59,6 @@ import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; -import javax.naming.OperationNotSupportedException; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.SoftAssertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - /** * Tests the extraction of entities from a {@link ResultSet} by the {@link EntityRowMapper}. * @@ -490,7 +492,7 @@ void columnNamesAreCaseInsensitive() throws SQLException { @Test // DATAJDBC-341 void immutableEmbeddedWithAllColumnsMissingShouldBeNull() throws SQLException { - ResultSet rs = mockResultSet(asList("ID"), // + ResultSet rs = mockResultSet(List.of("ID"), // ID_FOR_ENTITY_NOT_REFERENCING_MAP); rs.next(); @@ -564,7 +566,7 @@ void missingColumnsInEmbeddedShouldBeUnset() throws SQLException { @Test // DATAJDBC-341 void primitiveEmbeddedShouldBeNullWhenAllColumnsAreMissing() throws SQLException { - ResultSet rs = mockResultSet(asList("ID"), // + ResultSet rs = mockResultSet(List.of("ID"), // ID_FOR_ENTITY_NOT_REFERENCING_MAP); rs.next(); @@ -677,16 +679,17 @@ public String getName() { public boolean equals(final Object o) { if (o == this) return true; - if (!(o instanceof Trivial)) return false; - final Trivial other = (Trivial) o; - if (!other.canEqual((Object) this)) return false; + if (!(o instanceof final Trivial other)) + return false; + if (!other.canEqual(this)) + return false; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false; + if (!Objects.equals(this$id, other$id)) + return false; final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false; - return true; + return Objects.equals(this$name, other$name); } protected boolean canEqual(final Object other) { @@ -746,22 +749,24 @@ public long getReferenceToCustomer() { public boolean equals(final Object o) { if (o == this) return true; - if (!(o instanceof TrivialMapPropertiesToNullIfNotNeeded)) return false; - final TrivialMapPropertiesToNullIfNotNeeded other = (TrivialMapPropertiesToNullIfNotNeeded) o; - if (!other.canEqual((Object) this)) return false; + if (!(o instanceof final TrivialMapPropertiesToNullIfNotNeeded other)) + return false; + if (!other.canEqual(this)) + return false; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false; + if (!Objects.equals(this$id, other$id)) + return false; if (this.getAge() != other.getAge()) return false; final Object this$phone = this.getPhone(); final Object other$phone = other.getPhone(); - if (this$phone == null ? other$phone != null : !this$phone.equals(other$phone)) return false; + if (!Objects.equals(this$phone, other$phone)) + return false; final Object this$isSupreme = this.getIsSupreme(); final Object other$isSupreme = other.getIsSupreme(); - if (this$isSupreme == null ? other$isSupreme != null : !this$isSupreme.equals(other$isSupreme)) + if (!Objects.equals(this$isSupreme, other$isSupreme)) return false; - if (this.getReferenceToCustomer() != other.getReferenceToCustomer()) return false; - return true; + return this.getReferenceToCustomer() == other.getReferenceToCustomer(); } protected boolean canEqual(final Object other) { @@ -814,20 +819,21 @@ public AggregateReference getTrivialId() { public boolean equals(final Object o) { if (o == this) return true; - if (!(o instanceof WithReference)) return false; - final WithReference other = (WithReference) o; - if (!other.canEqual((Object) this)) return false; + if (!(o instanceof final WithReference other)) + return false; + if (!other.canEqual(this)) + return false; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false; + if (!Objects.equals(this$id, other$id)) + return false; final Object this$name = this.getName(); final Object other$name = other.getName(); - if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false; + if (!Objects.equals(this$name, other$name)) + return false; final Object this$trivialId = this.getTrivialId(); final Object other$trivialId = other.getTrivialId(); - if (this$trivialId == null ? other$trivialId != null : !this$trivialId.equals(other$trivialId)) - return false; - return true; + return Objects.equals(this$trivialId, other$trivialId); } protected boolean canEqual(final Object other) { @@ -1059,7 +1065,7 @@ private EntityRowMapper createRowMapper(Class type, NamingStrategy nam doReturn(simpleEntriesWithInts).when(accessStrategy) .findAllByPath(identifierOfValue(ID_FOR_ENTITY_REFERENCING_LIST), any(PersistentPropertyPath.class)); - BasicJdbcConverter converter = new BasicJdbcConverter(context, accessStrategy, new JdbcCustomConversions(), + MappingJdbcConverter converter = new MappingJdbcConverter(context, accessStrategy, new JdbcCustomConversions(), JdbcTypeFactory.unsupported(), IdentifierProcessing.ANSI); return new EntityRowMapper<>( // @@ -1109,7 +1115,7 @@ private static List> convertValues(List columns, Obj private static class ResultSetAnswer implements Answer { - private List names; + private final List names; private final List> values; private int index = -1; @@ -1314,10 +1320,10 @@ private interface SetExpectation { private static class FixtureBuilder implements SetValue, SetColumns, SetExpectation { - private List values = new ArrayList<>(); - private List columns = new ArrayList<>(); + private final List values = new ArrayList<>(); + private final List columns = new ArrayList<>(); private String explainingColumn; - private List> expectations = new ArrayList<>(); + private final List> expectations = new ArrayList<>(); @Override public SetColumns value(Object value) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java index f8f237e430..a6dbc9c717 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java @@ -36,7 +36,7 @@ public class JdbcIdentifierBuilderUnitTests { JdbcMappingContext context = new JdbcMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverterUnitTests.java similarity index 98% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverterUnitTests.java index 4dc22631b2..ee716fea6d 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverterUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverterUnitTests.java @@ -45,15 +45,15 @@ import org.springframework.data.util.TypeInformation; /** - * Unit tests for {@link BasicJdbcConverter}. + * Unit tests for {@link MappingJdbcConverter}. * * @author Mark Paluch */ -public class BasicJdbcConverterUnitTests { +public class MappingJdbcConverterUnitTests { JdbcMappingContext context = new JdbcMappingContext(); StubbedJdbcTypeFactory typeFactory = new StubbedJdbcTypeFactory(); - BasicJdbcConverter converter = new BasicJdbcConverter( // + MappingJdbcConverter converter = new MappingJdbcConverter( // context, // (identifier, path) -> { throw new UnsupportedOperationException(); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/QueryMapperUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/QueryMapperUnitTests.java index d2526fc9f2..4e04816b30 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/QueryMapperUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/QueryMapperUnitTests.java @@ -28,7 +28,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.relational.core.dialect.PostgresDialect; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.sql.Condition; @@ -48,7 +47,7 @@ public class QueryMapperUnitTests { JdbcMappingContext context = new JdbcMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); + JdbcConverter converter = new MappingJdbcConverter(context, mock(RelationResolver.class)); QueryMapper mapper = new QueryMapper(converter); MapSqlParameterSource parameterSource = new MapSqlParameterSource(); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java index 87510b0a67..eaa4defb95 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java @@ -23,7 +23,6 @@ import java.util.function.Consumer; import org.assertj.core.api.SoftAssertions; - import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; @@ -214,7 +213,7 @@ private void threadedTest(String user, CountDownLatch latch, Consumer te private SqlGenerator configureSqlGenerator(NamingStrategy namingStrategy) { RelationalMappingContext context = new JdbcMappingContext(namingStrategy); - JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java index 25a97b85f6..a4ab377d43 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java @@ -42,7 +42,7 @@ public class SqlGeneratorEmbeddedUnitTests { private final RelationalMappingContext context = new JdbcMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); private SqlGenerator sqlGenerator; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java index 5784dc0959..17960b412f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -19,7 +19,6 @@ import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; - import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -197,7 +196,7 @@ private PersistentPropertyPath getPath(String path private SqlGenerator configureSqlGenerator(NamingStrategy namingStrategy) { context = new JdbcMappingContext(namingStrategy); - JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index 61a3856ab6..f2c295828d 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -79,7 +79,7 @@ class SqlGeneratorUnitTests { private final PrefixingNamingStrategy namingStrategy = new PrefixingNamingStrategy(); private RelationalMappingContext context = new JdbcMappingContext(namingStrategy); - private final JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + private final JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); private SqlGenerator sqlGenerator; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java index a54383b3c9..63dfecff10 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Objects; import org.junit.jupiter.api.Test; import org.springframework.core.convert.converter.Converter; @@ -47,7 +48,7 @@ class SqlParametersFactoryTest { RelationalMappingContext context = new JdbcMappingContext(); RelationResolver relationResolver = mock(RelationResolver.class); - BasicJdbcConverter converter = new BasicJdbcConverter(context, relationResolver); + MappingJdbcConverter converter = new MappingJdbcConverter(context, relationResolver); AnsiDialect dialect = AnsiDialect.INSTANCE; SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(context, converter); @@ -210,14 +211,11 @@ public String getId() { public boolean equals(final Object o) { if (o == this) return true; - if (!(o instanceof IdValue)) + if (!(o instanceof final IdValue other)) return false; - final IdValue other = (IdValue) o; final Object this$id = this.getId(); final Object other$id = other.getId(); - if (this$id == null ? other$id != null : !this$id.equals(other$id)) - return false; - return true; + return Objects.equals(this$id, other$id); } public int hashCode() { @@ -301,7 +299,7 @@ public WithIllegalCharacters(Long id, String value) { private SqlParametersFactory createSqlParametersFactoryWithConverters(List converters) { - BasicJdbcConverter converter = new BasicJdbcConverter(context, relationResolver, + MappingJdbcConverter converter = new MappingJdbcConverter(context, relationResolver, new JdbcCustomConversions(converters), new DefaultJdbcTypeFactory(mock(JdbcOperations.class)), dialect.getIdentifierProcessing()); return new SqlParametersFactory(context, converter); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisCustomizingNamespaceHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisCustomizingNamespaceHsqlIntegrationTests.java index 37725dcb30..b86e52f75f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisCustomizingNamespaceHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisCustomizingNamespaceHsqlIntegrationTests.java @@ -33,8 +33,8 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; import org.springframework.data.jdbc.testing.DatabaseType; @@ -116,7 +116,7 @@ SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory factory) { MyBatisDataAccessStrategy dataAccessStrategy(SqlSession sqlSession) { RelationalMappingContext context = new JdbcMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(context, (Identifier, path) -> null); + JdbcConverter converter = new MappingJdbcConverter(context, (Identifier, path) -> null); MyBatisDataAccessStrategy strategy = new MyBatisDataAccessStrategy(sqlSession, HsqlDbDialect.INSTANCE.getIdentifierProcessing()); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index e94127e737..a9541421ce 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -33,15 +33,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; -import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; -import org.springframework.data.jdbc.core.convert.DelegatingDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.InsertStrategyFactory; -import org.springframework.data.jdbc.core.convert.JdbcConverter; -import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; -import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; -import org.springframework.data.jdbc.core.convert.SqlParametersFactory; +import org.springframework.data.jdbc.core.convert.*; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; @@ -67,6 +59,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; + /** * Unit tests for application events via {@link SimpleJdbcRepository}. * @@ -95,8 +88,8 @@ void before() { Dialect dialect = HsqlDbDialect.INSTANCE; DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy(); - JdbcConverter converter = new BasicJdbcConverter(context, delegatingDataAccessStrategy, new JdbcCustomConversions(), - new DefaultJdbcTypeFactory(operations.getJdbcOperations()), dialect.getIdentifierProcessing()); + JdbcConverter converter = new MappingJdbcConverter(context, delegatingDataAccessStrategy, + new JdbcCustomConversions(), new DefaultJdbcTypeFactory(operations.getJdbcOperations())); SqlGeneratorSource generatorSource = new SqlGeneratorSource(context, converter, dialect); SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(context, converter); InsertStrategyFactory insertStrategyFactory = new InsertStrategyFactory(operations, dialect); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 8c13c68de6..b5a03c2a92 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -30,8 +30,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -70,7 +70,7 @@ public class PartTreeJdbcQueryUnitTests { private static final String BASE_SELECT = "SELECT " + ALL_FIELDS + " " + JOIN_CLAUSE; JdbcMappingContext mappingContext = new JdbcMappingContext(); - JdbcConverter converter = new BasicJdbcConverter(mappingContext, mock(RelationResolver.class)); + JdbcConverter converter = new MappingJdbcConverter(mappingContext, mock(RelationResolver.class)); ReturnedType returnedType = mock(ReturnedType.class); @Test // DATAJDBC-318 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index 30dc8d5860..0f2a3f6176 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -39,15 +39,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.JdbcTypeFactory; +import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.mapping.JdbcValue; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; @@ -86,7 +85,7 @@ void setup() { this.defaultRowMapper = mock(RowMapper.class); this.operations = mock(NamedParameterJdbcOperations.class); this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS); - this.converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); + this.converter = new MappingJdbcConverter(context, mock(RelationResolver.class)); this.evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); } @@ -242,7 +241,7 @@ private class QueryFixture { private final JdbcQueryMethod method; private Object[] arguments; - private BasicJdbcConverter converter; + private MappingJdbcConverter converter; public QueryFixture(JdbcQueryMethod method) { this.method = method; @@ -257,8 +256,8 @@ public QueryFixture withArguments(Object... arguments) { public SqlParameterSource extractParameterSource() { - BasicJdbcConverter converter = this.converter == null // - ? new BasicJdbcConverter(mock(RelationalMappingContext.class), // + MappingJdbcConverter converter = this.converter == null // + ? new MappingJdbcConverter(mock(RelationalMappingContext.class), // mock(RelationResolver.class)) : this.converter; @@ -273,7 +272,7 @@ public SqlParameterSource extractParameterSource() { return captor.getValue(); } - public QueryFixture withConverter(BasicJdbcConverter converter) { + public QueryFixture withConverter(MappingJdbcConverter converter) { this.converter = converter; @@ -282,8 +281,8 @@ public QueryFixture withConverter(BasicJdbcConverter converter) { public QueryFixture withCustomConverters(Object... converters) { - return withConverter(new BasicJdbcConverter(mock(RelationalMappingContext.class), mock(RelationResolver.class), - new JdbcCustomConversions(List.of(converters)), JdbcTypeFactory.unsupported(), IdentifierProcessing.ANSI)); + return withConverter(new MappingJdbcConverter(mock(RelationalMappingContext.class), mock(RelationResolver.class), + new JdbcCustomConversions(List.of(converters)), JdbcTypeFactory.unsupported())); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java index c7824aaa73..3a882c5dcc 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java @@ -34,9 +34,9 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.relational.core.dialect.Dialect; @@ -89,7 +89,7 @@ public void setsUpBasicInstanceCorrectly() { factoryBean.setDataAccessStrategy(dataAccessStrategy); factoryBean.setMappingContext(mappingContext); - factoryBean.setConverter(new BasicJdbcConverter(mappingContext, dataAccessStrategy)); + factoryBean.setConverter(new MappingJdbcConverter(mappingContext, dataAccessStrategy)); factoryBean.setApplicationEventPublisher(publisher); factoryBean.setBeanFactory(beanFactory); factoryBean.setDialect(dialect); @@ -115,7 +115,7 @@ public void afterPropertiesThrowsExceptionWhenNoMappingContextSet() { public void afterPropertiesSetDefaultsNullablePropertiesCorrectly() { factoryBean.setMappingContext(mappingContext); - factoryBean.setConverter(new BasicJdbcConverter(mappingContext, dataAccessStrategy)); + factoryBean.setConverter(new MappingJdbcConverter(mappingContext, dataAccessStrategy)); factoryBean.setApplicationEventPublisher(publisher); factoryBean.setBeanFactory(beanFactory); factoryBean.setDialect(dialect); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 26041a201a..ad6f0b30f8 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -157,12 +157,11 @@ JdbcConverter relationalConverter(RelationalMappingContext mappingContext, @Lazy JdbcArrayColumns arrayColumns = dialect instanceof JdbcDialect ? ((JdbcDialect) dialect).getArraySupport() : JdbcArrayColumns.DefaultSupport.INSTANCE; - return new BasicJdbcConverter( // + return new MappingJdbcConverter( // mappingContext, // relationResolver, // conversions, // - new DefaultJdbcTypeFactory(template.getJdbcOperations(), arrayColumns), // - dialect.getIdentifierProcessing()); + new DefaultJdbcTypeFactory(template.getJdbcOperations(), arrayColumns)); } @Bean diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java index fe13e598de..4bfba1ca9f 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java @@ -31,26 +31,15 @@ import java.util.Optional; import java.util.function.BiFunction; -import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.convert.CustomConversions; import org.springframework.data.mapping.IdentifierAccessor; -import org.springframework.data.mapping.InstanceCreatorMetadata; -import org.springframework.data.mapping.MappingException; -import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; -import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; -import org.springframework.data.mapping.model.ParameterValueProvider; -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.r2dbc.mapping.OutboundRow; import org.springframework.data.r2dbc.support.ArrayUtils; import org.springframework.data.relational.core.conversion.MappingRelationalConverter; -import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.dialect.ArrayColumns; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -117,36 +106,13 @@ && getConversionService().canConvert(Row.class, rawType)) { return getConversionService().convert(row, rawType); } - return read(getRequiredPersistentEntity(type), row, metadata); - } - - private R read(RelationalPersistentEntity entity, Row row, @Nullable RowMetadata metadata) { - - R result = createInstance(row, metadata, "", entity); - - if (entity.requiresPropertyPopulation()) { - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>( - entity.getPropertyAccessor(result), getConversionService()); - - for (RelationalPersistentProperty property : entity) { - - if (entity.isCreatorArgument(property)) { - continue; - } - - Object value = readFrom(row, metadata, property, ""); - - if (value != null) { - propertyAccessor.setProperty(property, value); - } - } - } - - return result; + RowDocument document = toRowDocument(type, row, metadata != null ? metadata.getColumnMetadatas() : null); + return read(type, document); } @Override - public RowDocument toRowDocument(Class type, Readable row, Iterable metadata) { + public RowDocument toRowDocument(Class type, Readable row, + @Nullable Iterable metadata) { RowDocument document = new RowDocument(); RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(type); @@ -155,26 +121,28 @@ public RowDocument toRowDocument(Class type, Readable row, Iterable metadata, + private static void captureRowValues(Readable row, @Nullable Iterable metadata, RowDocument document, RelationalPersistentEntity persistentEntity) { for (RelationalPersistentProperty property : persistentEntity) { String identifier = property.getColumnName().getReference(); - if (property.isEntity() || !RowMetadataUtils.containsColumn(metadata, identifier)) { + if (property.isEntity() || (metadata != null && !RowMetadataUtils.containsColumn(metadata, identifier))) { continue; } @@ -193,194 +161,6 @@ private static void captureRowValues(Readable row, Iterable type) { - - if (null == value) { - return null; - } - - if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) { - return getConversionService().convert(value, type.getType()); - } else if (value instanceof Collection || value.getClass().isArray()) { - return readCollectionOrArray(asCollection(value), type); - } else { - return getPotentiallyConvertedSimpleRead(value, type.getType()); - } - } - - /** - * Reads the given value into a collection of the given {@link TypeInformation}. - * - * @param source must not be {@literal null}. - * @param targetType must not be {@literal null}. - * @return the converted {@link Collection} or array, will never be {@literal null}. - */ - @SuppressWarnings("unchecked") - private Object readCollectionOrArray(Collection source, TypeInformation targetType) { - - Assert.notNull(targetType, "Target type must not be null"); - - Class collectionType = targetType.isSubTypeOf(Collection.class) // - ? targetType.getType() // - : List.class; - - TypeInformation componentType = targetType.getComponentType() != null // - ? targetType.getComponentType() // - : TypeInformation.OBJECT; - Class rawComponentType = componentType.getType(); - - Collection items = targetType.getType().isArray() // - ? new ArrayList<>(source.size()) // - : CollectionFactory.createCollection(collectionType, rawComponentType, source.size()); - - if (source.isEmpty()) { - return getPotentiallyConvertedSimpleRead(items, targetType.getType()); - } - - for (Object element : source) { - - if (!Object.class.equals(rawComponentType) && element instanceof Collection) { - if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) { - throw new MappingException(String.format( - "Cannot convert %1$s of type %2$s into an instance of %3$s; Implement a custom Converter<%2$s, %3$s> and register it with the CustomConversions", - element, element.getClass(), rawComponentType)); - } - } - if (element instanceof List) { - items.add(readCollectionOrArray((Collection) element, componentType)); - } else { - items.add(getPotentiallyConvertedSimpleRead(element, rawComponentType)); - } - } - - return getPotentiallyConvertedSimpleRead(items, targetType.getType()); - } - - /** - * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies - * {@link Enum} handling or returns the value as is. - * - * @param value - * @param target must not be {@literal null}. - * @return - */ - @Nullable - @SuppressWarnings({ "rawtypes", "unchecked" }) - private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { - - if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { - return value; - } - - if (getConversions().hasCustomReadTarget(value.getClass(), target)) { - return getConversionService().convert(value, target); - } - - if (Enum.class.isAssignableFrom(target)) { - return Enum.valueOf((Class) target, value.toString()); - } - - return getConversionService().convert(value, target); - } - - @SuppressWarnings("unchecked") - private S readEntityFrom(Row row, @Nullable RowMetadata metadata, PersistentProperty property) { - - String prefix = property.getName() + "_"; - - RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(property.getActualType()); - - if (entity.hasIdProperty()) { - if (readFrom(row, metadata, entity.getRequiredIdProperty(), prefix) == null) { - return null; - } - } - - Object instance = createInstance(row, metadata, prefix, entity); - - if (entity.requiresPropertyPopulation()) { - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>(accessor, - getConversionService()); - - for (RelationalPersistentProperty p : entity) { - if (!entity.isCreatorArgument(property)) { - propertyAccessor.setProperty(p, readFrom(row, metadata, p, prefix)); - } - } - } - - return (S) instance; - } - - private S createInstance(Row row, @Nullable RowMetadata rowMetadata, String prefix, - RelationalPersistentEntity entity) { - - InstanceCreatorMetadata persistenceConstructor = entity.getInstanceCreatorMetadata(); - ParameterValueProvider provider; - - if (persistenceConstructor != null && persistenceConstructor.hasParameters()) { - - SpELContext spELContext = new SpELContext(new RowPropertyAccessor(rowMetadata)); - SpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(row, spELContext); - provider = new SpELExpressionParameterValueProvider<>(expressionEvaluator, getConversionService(), - new RowParameterValueProvider(row, rowMetadata, entity, this, prefix)); - } else { - provider = NoOpParameterValueProvider.INSTANCE; - } - - return createInstance(entity, provider::getParameterValue); - } - // ---------------------------------- // Entity writing // ---------------------------------- @@ -732,57 +512,4 @@ private static Collection asCollection(Object source) { return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); } - enum NoOpParameterValueProvider implements ParameterValueProvider { - - INSTANCE; - - @Override - public T getParameterValue( - org.springframework.data.mapping.Parameter parameter) { - return null; - } - } - - private class RowParameterValueProvider implements ParameterValueProvider { - - private final Row resultSet; - private final RowMetadata metadata; - private final RelationalPersistentEntity entity; - private final RelationalConverter converter; - private final String prefix; - - public RowParameterValueProvider(Row resultSet, RowMetadata metadata, RelationalPersistentEntity entity, - RelationalConverter converter, String prefix) { - this.resultSet = resultSet; - this.metadata = metadata; - this.entity = entity; - this.converter = converter; - this.prefix = prefix; - } - - @Override - @Nullable - public T getParameterValue( - org.springframework.data.mapping.Parameter parameter) { - - RelationalPersistentProperty property = this.entity.getRequiredPersistentProperty(parameter.getName()); - Object value = readFrom(this.resultSet, this.metadata, property, this.prefix); - - if (value == null) { - return null; - } - - Class type = parameter.getType().getType(); - - if (type.isInstance(value)) { - return type.cast(value); - } - - try { - return this.converter.getConversionService().convert(value, type); - } catch (Exception o_O) { - throw new MappingException(String.format("Couldn't read parameter %s", parameter.getName()), o_O); - } - } - } } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java index a774641bbe..3c858a6d33 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java @@ -25,11 +25,9 @@ import org.springframework.core.convert.ConversionService; import org.springframework.data.convert.EntityReader; import org.springframework.data.convert.EntityWriter; -import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.mapping.OutboundRow; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.dialect.ArrayColumns; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.RowDocument; @@ -42,13 +40,6 @@ public interface R2dbcConverter extends EntityReader, EntityWriter, RelationalConverter { - /** - * Returns the underlying {@link MappingContext} used by the converter. - * - * @return never {@literal null} - */ - MappingContext, ? extends RelationalPersistentProperty> getMappingContext(); - /** * Returns the underlying {@link ConversionService} used by the converter. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AbstractRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AbstractRelationalConverter.java new file mode 100644 index 0000000000..b1e302af46 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AbstractRelationalConverter.java @@ -0,0 +1,98 @@ +/* + * Copyright 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.relational.core.conversion; + +import java.util.Collections; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.CustomConversions.StoreConversions; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.util.Assert; + +/** + * Base class for {@link RelationalConverter} implementations. Sets up a {@link ConfigurableConversionService} and + * populates basic converters. Allows registering {@link CustomConversions}. + * + * @author Mark Paluch + * @since 3.2 + */ +public abstract class AbstractRelationalConverter implements RelationalConverter { + + private final RelationalMappingContext context; + private final ConfigurableConversionService conversionService; + private final EntityInstantiators entityInstantiators; + private final CustomConversions conversions; + + /** + * Creates a new {@link AbstractRelationalConverter} given {@link MappingContext}. + * + * @param context must not be {@literal null}. + */ + public AbstractRelationalConverter(RelationalMappingContext context) { + this(context, new CustomConversions(StoreConversions.NONE, Collections.emptyList()), new DefaultConversionService(), + new EntityInstantiators()); + } + + /** + * Creates a new {@link AbstractRelationalConverter} given {@link MappingContext} and {@link CustomConversions}. + * + * @param context must not be {@literal null}. + * @param conversions must not be {@literal null}. + */ + public AbstractRelationalConverter(RelationalMappingContext context, CustomConversions conversions) { + this(context, conversions, new DefaultConversionService(), new EntityInstantiators()); + } + + @SuppressWarnings("unchecked") + private AbstractRelationalConverter(RelationalMappingContext context, CustomConversions conversions, + ConfigurableConversionService conversionService, EntityInstantiators entityInstantiators) { + + Assert.notNull(context, "MappingContext must not be null"); + Assert.notNull(conversions, "CustomConversions must not be null"); + + this.context = context; + this.conversionService = conversionService; + this.entityInstantiators = entityInstantiators; + this.conversions = conversions; + + conversions.registerConvertersIn(this.conversionService); + } + + @Override + public ConversionService getConversionService() { + return conversionService; + } + + public CustomConversions getConversions() { + return conversions; + } + + @Override + public EntityInstantiators getEntityInstantiators() { + return entityInstantiators; + } + + @Override + public RelationalMappingContext getMappingContext() { + return context; + } + +} 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 05ffb29ab1..67f54864b1 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 @@ -15,40 +15,10 @@ */ package org.springframework.data.relational.core.conversion; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import org.springframework.core.ResolvableType; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.ConfigurableConversionService; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.convert.CustomConversions; -import org.springframework.data.convert.CustomConversions.StoreConversions; -import org.springframework.data.mapping.Parameter; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; -import org.springframework.data.mapping.model.EntityInstantiators; -import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.projection.EntityProjection; -import org.springframework.data.projection.ProjectionFactory; 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.domain.RowDocument; -import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * {@link RelationalConverter} that uses a {@link MappingContext} to apply basic conversion of relational values to @@ -63,13 +33,11 @@ * @see MappingContext * @see SimpleTypeHolder * @see CustomConversions + * @deprecated since 3.2, use {@link MappingRelationalConverter} instead as the naming suggests a limited scope of + * functionality. */ -public class BasicRelationalConverter implements RelationalConverter { - - private final RelationalMappingContext context; - private final ConfigurableConversionService conversionService; - private final EntityInstantiators entityInstantiators; - private final CustomConversions conversions; +@Deprecated(since = "3.2") +public class BasicRelationalConverter extends MappingRelationalConverter { /** * Creates a new {@link BasicRelationalConverter} given {@link MappingContext}. @@ -77,8 +45,7 @@ public class BasicRelationalConverter implements RelationalConverter { * @param context must not be {@literal null}. */ public BasicRelationalConverter(RelationalMappingContext context) { - this(context, new CustomConversions(StoreConversions.NONE, Collections.emptyList()), new DefaultConversionService(), - new EntityInstantiators()); + super(context); } /** @@ -88,259 +55,7 @@ public BasicRelationalConverter(RelationalMappingContext context) { * @param conversions must not be {@literal null}. */ public BasicRelationalConverter(RelationalMappingContext context, CustomConversions conversions) { - this(context, conversions, new DefaultConversionService(), new EntityInstantiators()); - } - - @SuppressWarnings("unchecked") - private BasicRelationalConverter(RelationalMappingContext context, CustomConversions conversions, - ConfigurableConversionService conversionService, EntityInstantiators entityInstantiators) { - - Assert.notNull(context, "MappingContext must not be null"); - Assert.notNull(conversions, "CustomConversions must not be null"); - - this.context = context; - this.conversionService = conversionService; - this.entityInstantiators = entityInstantiators; - this.conversions = conversions; - - conversions.registerConvertersIn(this.conversionService); - } - - @Override - public ConversionService getConversionService() { - return conversionService; - } - - public CustomConversions getConversions() { - return conversions; - } - - @Override - public RelationalMappingContext getMappingContext() { - return context; - } - - @Override - public PersistentPropertyPathAccessor getPropertyAccessor(PersistentEntity persistentEntity, - T instance) { - - PersistentPropertyPathAccessor accessor = persistentEntity.getPropertyPathAccessor(instance); - return new ConvertingPropertyAccessor<>(accessor, conversionService); - } - - @Override - public EntityProjection introspectProjection(Class resultType, Class entityType) { - throw new UnsupportedOperationException(); - } - - @Override - public ProjectionFactory getProjectionFactory() { - throw new UnsupportedOperationException(); - } - - @Override - public R project(EntityProjection descriptor, RowDocument document) { - throw new UnsupportedOperationException(); - } - - @Override - public R read(Class type, RowDocument source) { - throw new UnsupportedOperationException(); + super(context, conversions); } - @Override - public T createInstance(PersistentEntity entity, - Function, Object> parameterValueProvider) { - - return entityInstantiators.getInstantiatorFor(entity) // - .createInstance(entity, new ConvertingParameterValueProvider<>(parameterValueProvider)); - } - - @Override - @Nullable - public Object readValue(@Nullable Object value, TypeInformation type) { - - if (null == value) { - return null; - } - - if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) { - - TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(value.getClass()); - TypeDescriptor targetDescriptor = createTypeDescriptor(type); - - return getConversionService().convert(value, sourceDescriptor, targetDescriptor); - } - - return getPotentiallyConvertedSimpleRead(value, type); - } - - @Override - @Nullable - public Object writeValue(@Nullable Object value, TypeInformation type) { - - if (value == null) { - return null; - } - - if (getConversions().isSimpleType(value.getClass())) { - - if (TypeInformation.OBJECT != type && conversionService.canConvert(value.getClass(), type.getType())) { - value = conversionService.convert(value, type.getType()); - } - - return getPotentiallyConvertedSimpleWrite(value); - } - - if (value.getClass().isArray()) { - return writeArray(value, type); - } - - if (value instanceof Collection) { - return writeCollection((Iterable) value, type); - } - - RelationalPersistentEntity persistentEntity = context.getPersistentEntity(value.getClass()); - - if (persistentEntity != null) { - - Object id = persistentEntity.getIdentifierAccessor(value).getIdentifier(); - return writeValue(id, type); - } - - return conversionService.convert(value, type.getType()); - } - - private Object writeArray(Object value, TypeInformation type) { - - Class componentType = value.getClass().getComponentType(); - Optional> optionalWriteTarget = getConversions().getCustomWriteTarget(componentType); - - if (optionalWriteTarget.isEmpty() && !componentType.isEnum()) { - return value; - } - - Class customWriteTarget = optionalWriteTarget - .orElseGet(() -> componentType.isEnum() ? String.class : componentType); - - // optimization: bypass identity conversion - if (customWriteTarget.equals(componentType)) { - return value; - } - - TypeInformation component = TypeInformation.OBJECT; - if (type.isCollectionLike() && type.getActualType() != null) { - component = type.getRequiredComponentType(); - } - - int length = Array.getLength(value); - Object target = Array.newInstance(customWriteTarget, length); - for (int i = 0; i < length; i++) { - Array.set(target, i, writeValue(Array.get(value, i), component)); - } - - return target; - } - - private Object writeCollection(Iterable value, TypeInformation type) { - - List mapped = new ArrayList<>(); - - TypeInformation component = TypeInformation.OBJECT; - if (type.isCollectionLike() && type.getActualType() != null) { - component = type.getRequiredComponentType(); - } - - for (Object o : value) { - mapped.add(writeValue(o, component)); - } - - if (type.getType().isInstance(mapped) || !type.isCollectionLike()) { - return mapped; - } - - return conversionService.convert(mapped, type.getType()); - } - - @Override - public EntityInstantiators getEntityInstantiators() { - return this.entityInstantiators; - } - - /** - * Checks whether we have a custom conversion registered for the given value into an arbitrary simple JDBC type. - * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. - * - * @param value to be converted. Must not be {@code null}. - * @return the converted value if a conversion applies or the original value. Might return {@code null}. - */ - @Nullable - private Object getPotentiallyConvertedSimpleWrite(Object value) { - - Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); - - if (customTarget.isPresent()) { - return conversionService.convert(value, customTarget.get()); - } - - return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; - } - - /** - * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies - * {@link Enum} handling or returns the value as is. - * - * @param value to be converted. May be {@code null}.. - * @param type {@link TypeInformation} into which the value is to be converted. Must not be {@code null}. - * @return the converted value if a conversion applies or the original value. Might return {@code null}. - */ - @SuppressWarnings({ "rawtypes", "unchecked" }) - protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation type) { - - Class target = type.getType(); - if (ClassUtils.isAssignableValue(target, value)) { - return value; - } - - if (Enum.class.isAssignableFrom(target) && value instanceof CharSequence) { - return Enum.valueOf((Class) target, value.toString()); - } - - return conversionService.convert(value, TypeDescriptor.forObject(value), createTypeDescriptor(type)); - } - - private static TypeDescriptor createTypeDescriptor(TypeInformation type) { - - List> typeArguments = type.getTypeArguments(); - Class[] generics = new Class[typeArguments.size()]; - for (int i = 0; i < typeArguments.size(); i++) { - generics[i] = typeArguments.get(i).getType(); - } - - return new TypeDescriptor(ResolvableType.forClassWithGenerics(type.getType(), generics), type.getType(), null); - } - - /** - * Converter-aware {@link ParameterValueProvider}. - * - * @param

- * @author Mark Paluch - */ - class ConvertingParameterValueProvider

> implements ParameterValueProvider

{ - - private final Function, Object> delegate; - - ConvertingParameterValueProvider(Function, Object> delegate) { - - Assert.notNull(delegate, "Delegate must not be null"); - - this.delegate = delegate; - } - - @Override - @SuppressWarnings("unchecked") - public T getParameterValue(Parameter parameter) { - return (T) readValue(delegate.apply(parameter), parameter.getType()); - } - } } 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 30aaac0ce9..30a317e07a 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 @@ -15,18 +15,23 @@ */ package org.springframework.data.relational.core.conversion; +import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.function.Function; import java.util.function.Predicate; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.convert.CustomConversions; import org.springframework.data.mapping.InstanceCreatorMetadata; import org.springframework.data.mapping.MappingException; @@ -34,6 +39,7 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; @@ -41,6 +47,7 @@ import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.mapping.model.PropertyValueProvider; +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; @@ -69,9 +76,15 @@ * {@link RowDocument}. * * @author Mark Paluch + * @author Jens Schauder + * @author Chirag Tailor + * @author Vincent Galloy + * @see MappingContext + * @see SimpleTypeHolder + * @see CustomConversions * @since 3.2 */ -public class MappingRelationalConverter extends BasicRelationalConverter implements ApplicationContextAware { +public class MappingRelationalConverter extends AbstractRelationalConverter implements ApplicationContextAware { private SpELContext spELContext; @@ -124,11 +137,6 @@ public void setApplicationContext(ApplicationContext applicationContext) throws this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); } - @Override - public ProjectionFactory getProjectionFactory() { - return this.projectionFactory; - } - /** * Creates a new {@link ConversionContext}. * @@ -142,6 +150,14 @@ protected ConversionContext getConversionContext(ObjectPath path) { this::readMap, this::getPotentiallyConvertedSimpleRead); } + @Override + public PersistentPropertyPathAccessor getPropertyAccessor(PersistentEntity persistentEntity, + T instance) { + + PersistentPropertyPathAccessor accessor = persistentEntity.getPropertyPathAccessor(instance); + return new ConvertingPropertyAccessor<>(accessor, getConversionService()); + } + @Override public EntityProjection introspectProjection(Class resultType, Class entityType) { @@ -315,7 +331,7 @@ protected S readAggregate(ConversionContext context, RowDocum } if (RowDocument.class.isAssignableFrom(rawType)) { - return (S) documentAccessor; + return (S) documentAccessor.document(); } if (typeHint.isMap()) { @@ -445,6 +461,14 @@ private S read(ConversionContext context, RelationalPersistentEntity enti return instance; } + @Override + public T createInstance(PersistentEntity entity, + Function, Object> parameterValueProvider) { + + return getEntityInstantiators().getInstantiatorFor(entity) // + .createInstance(entity, new ConvertingParameterValueProvider<>(parameterValueProvider)); + } + private ParameterValueProvider getParameterProvider(ConversionContext context, RelationalPersistentEntity entity, RowDocumentAccessor source, SpELExpressionEvaluator evaluator) { @@ -568,6 +592,165 @@ private boolean shouldReadEmbeddable(ConversionContext context, RelationalPersis return false; } + @Override + @Nullable + public Object readValue(@Nullable Object value, TypeInformation type) { + + if (null == value) { + return null; + } + + if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) { + + TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(value.getClass()); + TypeDescriptor targetDescriptor = createTypeDescriptor(type); + + return getConversionService().convert(value, sourceDescriptor, targetDescriptor); + } + + return getPotentiallyConvertedSimpleRead(value, type); + } + + /** + * Checks whether we have a custom conversion registered for the given value into an arbitrary simple JDBC type. + * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. + * + * @param value to be converted. Must not be {@code null}. + * @return the converted value if a conversion applies or the original value. Might return {@code null}. + */ + @Nullable + private Object getPotentiallyConvertedSimpleWrite(Object value) { + + Optional> customTarget = getConversions().getCustomWriteTarget(value.getClass()); + + if (customTarget.isPresent()) { + return getConversionService().convert(value, customTarget.get()); + } + + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; + } + + /** + * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies + * {@link Enum} handling or returns the value as is. + * + * @param value to be converted. May be {@code null}.. + * @param type {@link TypeInformation} into which the value is to be converted. Must not be {@code null}. + * @return the converted value if a conversion applies or the original value. Might return {@code null}. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation type) { + + Class target = type.getType(); + if (ClassUtils.isAssignableValue(target, value)) { + return value; + } + + if (Enum.class.isAssignableFrom(target) && value instanceof CharSequence) { + return Enum.valueOf((Class) target, value.toString()); + } + + return getConversionService().convert(value, TypeDescriptor.forObject(value), createTypeDescriptor(type)); + } + + private static TypeDescriptor createTypeDescriptor(TypeInformation type) { + + List> typeArguments = type.getTypeArguments(); + Class[] generics = new Class[typeArguments.size()]; + for (int i = 0; i < typeArguments.size(); i++) { + generics[i] = typeArguments.get(i).getType(); + } + + return new TypeDescriptor(ResolvableType.forClassWithGenerics(type.getType(), generics), type.getType(), null); + } + + @Override + @Nullable + public Object writeValue(@Nullable Object value, TypeInformation type) { + + if (value == null) { + return null; + } + + if (getConversions().isSimpleType(value.getClass())) { + + if (TypeInformation.OBJECT != type && getConversionService().canConvert(value.getClass(), type.getType())) { + value = getConversionService().convert(value, type.getType()); + } + + return getPotentiallyConvertedSimpleWrite(value); + } + + if (value.getClass().isArray()) { + return writeArray(value, type); + } + + if (value instanceof Collection) { + return writeCollection((Iterable) value, type); + } + + RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(value.getClass()); + + if (persistentEntity != null) { + + Object id = persistentEntity.getIdentifierAccessor(value).getIdentifier(); + return writeValue(id, type); + } + + return getConversionService().convert(value, type.getType()); + } + + private Object writeArray(Object value, TypeInformation type) { + + Class componentType = value.getClass().getComponentType(); + Optional> optionalWriteTarget = getConversions().getCustomWriteTarget(componentType); + + if (optionalWriteTarget.isEmpty() && !componentType.isEnum()) { + return value; + } + + Class customWriteTarget = optionalWriteTarget + .orElseGet(() -> componentType.isEnum() ? String.class : componentType); + + // optimization: bypass identity conversion + if (customWriteTarget.equals(componentType)) { + return value; + } + + TypeInformation component = TypeInformation.OBJECT; + if (type.isCollectionLike() && type.getActualType() != null) { + component = type.getRequiredComponentType(); + } + + int length = Array.getLength(value); + Object target = Array.newInstance(customWriteTarget, length); + for (int i = 0; i < length; i++) { + Array.set(target, i, writeValue(Array.get(value, i), component)); + } + + return target; + } + + private Object writeCollection(Iterable value, TypeInformation type) { + + List mapped = new ArrayList<>(); + + TypeInformation component = TypeInformation.OBJECT; + if (type.isCollectionLike() && type.getActualType() != null) { + component = type.getRequiredComponentType(); + } + + for (Object o : value) { + mapped.add(writeValue(o, component)); + } + + if (type.getType().isInstance(mapped) || !type.isCollectionLike()) { + return mapped; + } + + return getConversionService().convert(mapped, type.getType()); + } + static Predicate isConstructorArgument(PersistentEntity entity) { return entity::isCreatorArgument; } @@ -805,13 +988,16 @@ public T getParameterValue(Parameter parame } } - // TODO: Docs + /** + * Extended {@link ParameterValueProvider} that can report whether a property value is present and contextualize the + * instance for specific behavior like projection mapping in the context of a property. + */ protected interface RelationalPropertyValueProvider extends PropertyValueProvider { /** * Determine whether there is a value for the given {@link RelationalPersistentProperty}. * - * @param property + * @param property the property to check for whether a value is present. * @return */ boolean hasValue(RelationalPersistentProperty property); @@ -819,7 +1005,7 @@ protected interface RelationalPropertyValueProvider extends PropertyValueProvide /** * Contextualize this property value provider. * - * @param context + * @param context the context to use. * @return */ RelationalPropertyValueProvider withContext(ConversionContext context); @@ -834,15 +1020,21 @@ protected interface AggregatePathValueProvider extends RelationalPropertyValuePr /** * Determine whether there is a value for the given {@link AggregatePath}. * - * @param path + * @param path the path to check for whether a value is present. * @return */ boolean hasValue(AggregatePath path); + /** + * Determine whether there is a value for the given {@link SqlIdentifier}. + * + * @param identifier the path to check for whether a value is present. + * @return + */ boolean hasValue(SqlIdentifier identifier); /** - * Returns a value for the given {@link AggregatePath}. + * Return a value for the given {@link AggregatePath}. * * @param path will never be {@literal null}. * @return @@ -868,9 +1060,13 @@ protected interface AggregatePathValueProvider extends RelationalPropertyValuePr * @author Mark Paluch * @author Christoph Strobl */ - protected record DocumentValueProvider(ConversionContext context, RowDocumentAccessor accessor, - SpELExpressionEvaluator evaluator, - SpELContext spELContext) implements RelationalPropertyValueProvider, AggregatePathValueProvider { + protected static final class DocumentValueProvider + implements RelationalPropertyValueProvider, AggregatePathValueProvider { + + private final ConversionContext context; + private final RowDocumentAccessor accessor; + private final SpELExpressionEvaluator evaluator; + private final SpELContext spELContext; /** * Creates a new {@link RelationalPropertyValueProvider} for the given source and {@link SpELExpressionEvaluator}. @@ -879,11 +1075,16 @@ protected record DocumentValueProvider(ConversionContext context, RowDocumentAcc * @param accessor must not be {@literal null}. * @param evaluator must not be {@literal null}. */ - protected DocumentValueProvider { + private DocumentValueProvider(ConversionContext context, RowDocumentAccessor accessor, + SpELExpressionEvaluator evaluator, SpELContext spELContext) { Assert.notNull(context, "ConversionContext must no be null"); Assert.notNull(accessor, "DocumentAccessor must no be null"); Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null"); + this.context = context; + this.accessor = accessor; + this.evaluator = evaluator; + this.spELContext = spELContext; } @Override @@ -936,6 +1137,38 @@ public DocumentValueProvider withContext(ConversionContext context) { return context == this.context ? this : new DocumentValueProvider(context, accessor, evaluator, spELContext); } + public ConversionContext context() { + return context; + } + + public RowDocumentAccessor accessor() { + return accessor; + } + + } + + /** + * Converter-aware {@link ParameterValueProvider}. + * + * @param

+ * @author Mark Paluch + */ + class ConvertingParameterValueProvider

> implements ParameterValueProvider

{ + + private final Function, Object> delegate; + + ConvertingParameterValueProvider(Function, Object> delegate) { + + Assert.notNull(delegate, "Delegate must not be null"); + + this.delegate = delegate; + } + + @Override + @SuppressWarnings("unchecked") + public T getParameterValue(Parameter parameter) { + return (T) readValue(delegate.apply(parameter), parameter.getType()); + } } /** diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java index c78d9ea61e..9a15d6864f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java @@ -27,7 +27,6 @@ import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjectionIntrospector; -import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.RowDocument; @@ -43,6 +42,21 @@ */ public interface RelationalConverter { + /** + * Returns the underlying {@link ConversionService} used by the converter. + * + * @return never {@literal null}. + */ + ConversionService getConversionService(); + + /** + * Return the underlying {@link EntityInstantiators}. + * + * @return + * @since 2.3 + */ + EntityInstantiators getEntityInstantiators(); + /** * Returns the underlying {@link MappingContext} used by the converter. * @@ -51,19 +65,30 @@ public interface RelationalConverter { MappingContext, ? extends RelationalPersistentProperty> getMappingContext(); /** - * Returns the underlying {@link ConversionService} used by the converter. + * Create a new instance of {@link PersistentEntity} given {@link ParameterValueProvider} to obtain constructor + * properties. * - * @return never {@literal null}. + * @param entity the kind of entity to create. Must not be {@code null}. + * @param parameterValueProvider a function that provides the value to pass to a constructor, given a + * {@link Parameter}. Must not be {@code null}. + * @param the type of entity to create. + * @return the instantiated entity. Guaranteed to be not {@code null}. + * @deprecated since 3.2, use {@link #read} method instead. */ - ConversionService getConversionService(); + @Deprecated(since = "3.2") + default T createInstance(PersistentEntity entity, + Function, Object> parameterValueProvider) { + throw new UnsupportedOperationException("Not supported anymore. Use read(…) instead."); + } /** - * Returns the {@link ProjectionFactory} for this converter. + * Return a {@link PersistentPropertyAccessor} to access property values of the {@code instance}. * - * @return will never be {@literal null}. - * @since 3.2 + * @param persistentEntity the kind of entity to operate on. Must not be {@code null}. + * @param instance the instance to operate on. Must not be {@code null}. + * @return guaranteed to be not {@code null}. */ - ProjectionFactory getProjectionFactory(); + PersistentPropertyPathAccessor getPropertyAccessor(PersistentEntity persistentEntity, T instance); /** * Introspect the given {@link Class result type} in the context of the {@link Class entity type} whether the returned @@ -101,28 +126,6 @@ public interface RelationalConverter { */ R read(Class type, RowDocument source); - /** - * Create a new instance of {@link PersistentEntity} given {@link ParameterValueProvider} to obtain constructor - * properties. - * - * @param entity the kind of entity to create. Must not be {@code null}. - * @param parameterValueProvider a function that provides the value to pass to a constructor, given a - * {@link Parameter}. Must not be {@code null}. - * @param the type of entity to create. - * @return the instantiated entity. Guaranteed to be not {@code null}. - */ - T createInstance(PersistentEntity entity, - Function, Object> parameterValueProvider); - - /** - * Return a {@link PersistentPropertyAccessor} to access property values of the {@code instance}. - * - * @param persistentEntity the kind of entity to operate on. Must not be {@code null}. - * @param instance the instance to operate on. Must not be {@code null}. - * @return guaranteed to be not {@code null}. - */ - PersistentPropertyPathAccessor getPropertyAccessor(PersistentEntity persistentEntity, T instance); - /** * Read a relational value into the desired {@link TypeInformation destination type}. * @@ -143,11 +146,5 @@ T createInstance(PersistentEntity entity, @Nullable Object writeValue(@Nullable Object value, TypeInformation type); - /** - * Return the underlying {@link EntityInstantiators}. - * - * @return - * @since 2.3 - */ - EntityInstantiators getEntityInstantiators(); + } From 0133c9a937027c4ed8f703b077d26b8d9b758315 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 12 Oct 2023 14:20:31 +0200 Subject: [PATCH 6/6] Fix identifier propagation for no-id list elements. --- .../convert/DefaultDataAccessStrategy.java | 26 +++++- ...JdbcAggregateTemplateIntegrationTests.java | 85 ++++++------------- 2 files changed, 47 insertions(+), 64 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index e1ad40cfff..6cd53f6b63 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -18,6 +18,7 @@ import static org.springframework.data.jdbc.core.convert.SqlGenerator.*; import java.sql.ResultSet; +import java.sql.SQLException; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -29,6 +30,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.AggregatePath; +import org.springframework.data.relational.core.mapping.AggregatePath.TableInfo; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -300,11 +302,27 @@ public Iterable findAllByPath(Identifier identifier, String findAllByProperty = sql(actualType) // .getFindAllByProperty(identifier, propertyPath); - RowMapper rowMapper = path.isMap() ? this.getMapEntityRowMapper(path, identifier) - : this.getEntityRowMapper(path, identifier); - SqlParameterSource parameterSource = sqlParametersFactory.forQueryByIdentifier(identifier); - return operations.query(findAllByProperty, parameterSource, (RowMapper) rowMapper); + return operations.query(findAllByProperty, parameterSource, new RowMapper<>() { + + @Override + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + if (path.isMap()) { + return getMapEntityRowMapper(path, identifier).mapRow(rs, rowNum); + } + + // Add row number as key for paths that do not defile an identifier and that are contained in a collection. + Identifier identifierToUse = identifier; + if (!path.hasIdProperty() && path.isQualified()) { + + TableInfo tableInfo = path.getTableInfo(); + identifierToUse = identifierToUse.withPart(tableInfo.qualifierColumnInfo().name(), rowNum, Object.class); + } + + return getEntityRowMapper(path, identifierToUse).mapRow(rs, rowNum); + } + }); } @Override 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 99f505b87e..da8a8a7855 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 @@ -883,6 +883,9 @@ void saveAndLoadLongChainOfListsWithoutIds() { assertThat(saved.four).describedAs("Something went wrong during saving").isNotNull(); NoIdListChain4 reloaded = template.findById(saved.four, NoIdListChain4.class); + + assertThat(reloaded.chain3).hasSameSizeAs(saved.chain3); + assertThat(reloaded.chain3.get(0).chain2).hasSameSizeAs(saved.chain3.get(0).chain2); assertThat(reloaded).isEqualTo(saved); } @@ -1537,11 +1540,8 @@ public int hashCode() { @Override public String toString() { - final StringBuffer sb = new StringBuffer(); - sb.append(getClass().getSimpleName()); - sb.append(" [zeroValue='").append(zeroValue).append('\''); - sb.append(']'); - return sb.toString(); + String sb = getClass().getSimpleName() + " [zeroValue='" + zeroValue + '\'' + ']'; + return sb; } } @@ -1566,12 +1566,8 @@ public int hashCode() { @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(); + String sb = getClass().getSimpleName() + " [oneValue='" + oneValue + '\'' + ", chain0=" + chain0 + ']'; + return sb; } } @@ -1596,12 +1592,8 @@ public int hashCode() { @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(); + String sb = getClass().getSimpleName() + " [twoValue='" + twoValue + '\'' + ", chain1=" + chain1 + ']'; + return sb; } } @@ -1626,12 +1618,8 @@ public int hashCode() { @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(); + String sb = getClass().getSimpleName() + " [threeValue='" + threeValue + '\'' + ", chain2=" + chain2 + ']'; + return sb; } } @@ -1658,13 +1646,9 @@ public int hashCode() { @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(); + String sb = getClass().getSimpleName() + " [four=" + four + ", fourValue='" + fourValue + '\'' + ", chain3=" + + chain3 + ']'; + return sb; } } @@ -1692,11 +1676,8 @@ public int hashCode() { @Override public String toString() { - final StringBuffer sb = new StringBuffer(); - sb.append(getClass().getSimpleName()); - sb.append(" [zeroValue='").append(zeroValue).append('\''); - sb.append(']'); - return sb.toString(); + String sb = getClass().getSimpleName() + " [zeroValue='" + zeroValue + '\'' + ']'; + return sb; } } @@ -1721,12 +1702,8 @@ public int hashCode() { @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(); + String sb = getClass().getSimpleName() + " [oneValue='" + oneValue + '\'' + ", chain0=" + chain0 + ']'; + return sb; } } @@ -1751,12 +1728,8 @@ public int hashCode() { @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(); + String sb = getClass().getSimpleName() + " [twoValue='" + twoValue + '\'' + ", chain1=" + chain1 + ']'; + return sb; } } @@ -1781,12 +1754,8 @@ public int hashCode() { @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(); + String sb = getClass().getSimpleName() + " [threeValue='" + threeValue + '\'' + ", chain2=" + chain2 + ']'; + return sb; } } @@ -1813,13 +1782,9 @@ public int hashCode() { @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(); + String sb = getClass().getSimpleName() + " [four=" + four + ", fourValue='" + fourValue + '\'' + ", chain3=" + + chain3 + ']'; + return sb; } }