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;
}
}
This commit is contained in:
@@ -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<Iterable<?>, 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<T> entity = (RelationalPersistentEntity<T>) 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<T> 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 <S> ReadingContext<S> extendBy(RelationalPersistentProperty property) {
|
||||
return new ReadingContext<>(
|
||||
(RelationalPersistentEntity<S>) 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<T, RelationalPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
|
||||
ParameterValueProvider<RelationalPersistentProperty> provider;
|
||||
|
||||
if (persistenceConstructor != null && persistenceConstructor.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<RelationalPersistentProperty> {
|
||||
|
||||
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> T getParameterValue(PreferredConstructor.Parameter<T, RelationalPersistentProperty> 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 readOrLoadProperty(idValue, property);
|
||||
});
|
||||
|
||||
return entity.requiresPropertyPopulation() ? populateProperties(instance, idValue) : instance;
|
||||
return (T) readOrLoadProperty(idValue, property);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isSimpleProperty(RelationalPersistentProperty property) {
|
||||
return !property.isCollectionLike() && !property.isEntity() && !property.isMap() && !property.isEmbedded();
|
||||
}
|
||||
|
||||
enum NoOpParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public <T> T getParameterValue(PreferredConstructor.Parameter<T, RelationalPersistentProperty> parameter) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user