From 9cd8fc16b35cf7bef2deea38d190213f6fcaceb0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Sep 2020 11:48:56 +0200 Subject: [PATCH] DATAJDBC-508 - Add support for @Value in persistence constructors. We now evaluate @Value annotations in persistence constructors to compute values when creating object instances. AtValue can be used to materialize values for e.g. transient properties. Root properties map to the ResultSet from which an object gets materialized. class WithAtValue { private final @Id Long id; private final @Transient String computed; public WithAtValue(Long id, @Value("#root.first_name") String computed) { // obtain value from first_name column this.id = id; this.computed = computed; } } --- .../jdbc/core/convert/BasicJdbcConverter.java | 93 ++++++++++++++++--- .../ResultSetAccessorPropertyAccessor.java | 89 ++++++++++++++++++ .../convert/EntityRowMapperUnitTests.java | 27 ++++++ src/main/asciidoc/new-features.adoc | 1 + 4 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetAccessorPropertyAccessor.java 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