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 a7170728e..7239e3cba 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 @@ -24,6 +24,9 @@ import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +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; @@ -33,7 +36,12 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PreferredConstructor; 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.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; @@ -60,7 +68,7 @@ import org.springframework.util.Assert; * @see CustomConversions * @since 1.1 */ -public class BasicJdbcConverter extends BasicRelationalConverter implements JdbcConverter { +public class BasicJdbcConverter extends BasicRelationalConverter implements JdbcConverter, ApplicationContextAware { private static final Logger LOG = LoggerFactory.getLogger(BasicJdbcConverter.class); private static final Converter, Map> ITERABLE_OF_ENTRY_TO_MAP_CONVERTER = new IterableOfEntryToMapConverter(); @@ -69,6 +77,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc private final IdentifierProcessing identifierProcessing; private final RelationResolver relationResolver; + private SpELContext spELContext; /** * Creates a new {@link BasicRelationalConverter} given {@link MappingContext} and a @@ -88,9 +97,10 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc Assert.notNull(relationResolver, "RelationResolver must not be null"); - this.relationResolver = relationResolver; this.typeFactory = JdbcTypeFactory.unsupported(); this.identifierProcessing = IdentifierProcessing.ANSI; + this.relationResolver = relationResolver; + this.spELContext = new SpELContext(ResultSetAccessorPropertyAccessor.INSTANCE); } /** @@ -113,9 +123,19 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc Assert.notNull(relationResolver, "RelationResolver must not be null"); Assert.notNull(identifierProcessing, "IdentifierProcessing must not be null"); - this.relationResolver = relationResolver; this.typeFactory = typeFactory; this.identifierProcessing = identifierProcessing; + this.relationResolver = relationResolver; + this.spELContext = new SpELContext(ResultSetAccessorPropertyAccessor.INSTANCE); + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.spELContext = new SpELContext(this.spELContext, applicationContext); } @Nullable @@ -344,11 +364,11 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc private final JdbcPropertyValueProvider propertyValueProvider; private final JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider; + private final ResultSetAccessor accessor; @SuppressWarnings("unchecked") private ReadingContext(PersistentPropertyPathExtension rootPath, ResultSetAccessor accessor, Identifier identifier, Object key) { - RelationalPersistentEntity entity = (RelationalPersistentEntity) rootPath.getLeafEntity(); Assert.notNull(entity, "The rootPath must point to an entity."); @@ -361,12 +381,13 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc this.propertyValueProvider = new JdbcPropertyValueProvider(identifierProcessing, path, accessor); this.backReferencePropertyValueProvider = new JdbcBackReferencePropertyValueProvider(identifierProcessing, path, accessor); + this.accessor = accessor; } private ReadingContext(RelationalPersistentEntity entity, PersistentPropertyPathExtension rootPath, PersistentPropertyPathExtension path, Identifier identifier, Object key, JdbcPropertyValueProvider propertyValueProvider, - JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider) { + JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider, ResultSetAccessor accessor) { this.entity = entity; this.rootPath = rootPath; this.path = path; @@ -374,13 +395,14 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc 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.extendBy(property), path.extendBy(property), identifier, key, - propertyValueProvider.extendBy(property), backReferencePropertyValueProvider.extendBy(property)); + propertyValueProvider.extendBy(property), backReferencePropertyValueProvider.extendBy(property), accessor); } T mapRow() { @@ -529,23 +551,70 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc private T createInstanceInternal(@Nullable Object idValue) { - T instance = createInstance(entity, parameter -> { + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + ParameterValueProvider provider; - String parameterName = parameter.getName(); + if (persistenceConstructor != null && persistenceConstructor.hasParameters()) { - Assert.notNull(parameterName, "A constructor parameter name must not be null to be used with Spring Data JDBC"); + SpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(accessor, spELContext); + provider = new SpELExpressionParameterValueProvider<>(expressionEvaluator, getConversionService(), + new ResultSetParameterValueProvider(idValue, entity)); + } else { + provider = NoOpParameterValueProvider.INSTANCE; + } - RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameterName); - return readOrLoadProperty(idValue, property); - }); + 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; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Override + @Nullable + public T getParameterValue(PreferredConstructor.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(PreferredConstructor.Parameter parameter) { + return null; + } + } + } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetAccessorPropertyAccessor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetAccessorPropertyAccessor.java new file mode 100644 index 000000000..067126133 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetAccessorPropertyAccessor.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 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 org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; + +/** + * {@link PropertyAccessor} to access a column from a {@link ResultSetAccessor}. + * + * @author Mark Paluch + * @since 2.1 + */ +class ResultSetAccessorPropertyAccessor implements PropertyAccessor { + + static final PropertyAccessor INSTANCE = new ResultSetAccessorPropertyAccessor(); + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#getSpecificTargetClasses() + */ + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] { ResultSetAccessor.class }; + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) + */ + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) { + return target instanceof ResultSetAccessor && ((ResultSetAccessor) target).hasValue(name); + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) + */ + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) { + + if (target == null) { + return TypedValue.NULL; + } + + Object value = ((ResultSetAccessor) target).getObject(name); + + if (value == null) { + return TypedValue.NULL; + } + + return new TypedValue(value); + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#canWrite(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) + */ + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.expression.PropertyAccessor#write(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.lang.Object) + */ + @Override + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) { + throw new UnsupportedOperationException(); + } + +} 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 95d9409f2..2345816a0 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 @@ -51,6 +51,7 @@ import org.mockito.stubbing.Answer; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.Transient; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.mapping.PersistentPropertyPath; @@ -642,6 +643,19 @@ public class EntityRowMapperUnitTests { assertThat(result.child).isNull(); } + @Test // DATAJDBC-508 + public void materializesObjectWithAtValue() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "FIRST_NAME"), // + 123L, "Hello World"); + rs.next(); + + WithAtValue result = createRowMapper(WithAtValue.class).mapRow(rs, 1); + + assertThat(result.getId()).isEqualTo(123L); + assertThat(result.getComputed()).isEqualTo("Hello World"); + } + // Model classes to be used in tests @With @@ -1221,4 +1235,17 @@ public class EntityRowMapperUnitTests { final Object expectedValue; final String sourceColumn; } + + @Getter + private static class WithAtValue { + + @Id private final Long id; + private final @Transient String computed; + + public WithAtValue(Long id, + @org.springframework.beans.factory.annotation.Value("#root.first_name") String computed) { + this.id = id; + this.computed = computed; + } + } } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index af671875d..70961eb2c 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,6 +7,7 @@ This section covers the significant changes for each version. == What's New in Spring Data JDBC 2.1 * Dialect for Oracle databases. +* Support for `@Value` in persistence constructors. [[new-features.2-0-0]] == What's New in Spring Data JDBC 2.0