Browse Source

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;
		}
	}
pull/247/head
Mark Paluch 5 years ago
parent
commit
9cd8fc16b3
No known key found for this signature in database
GPG Key ID: 51A00FA751B91849
  1. 93
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java
  2. 89
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetAccessorPropertyAccessor.java
  3. 27
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java
  4. 1
      src/main/asciidoc/new-features.adoc

93
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java

@ -24,6 +24,9 @@ import java.util.Optional; @@ -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; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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;
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<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 (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;
}
}
}

89
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetAccessorPropertyAccessor.java

@ -0,0 +1,89 @@ @@ -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();
}
}

27
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java

@ -51,6 +51,7 @@ import org.mockito.stubbing.Answer; @@ -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 { @@ -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 { @@ -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;
}
}
}

1
src/main/asciidoc/new-features.adoc

@ -7,6 +7,7 @@ This section covers the significant changes for each version. @@ -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

Loading…
Cancel
Save