Skip to content

Commit 9cd8fc1

Browse files
committed
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; } }
1 parent 842a309 commit 9cd8fc1

File tree

4 files changed

+198
-12
lines changed

4 files changed

+198
-12
lines changed

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

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424

2525
import org.slf4j.Logger;
2626
import org.slf4j.LoggerFactory;
27+
28+
import org.springframework.context.ApplicationContext;
29+
import org.springframework.context.ApplicationContextAware;
2730
import org.springframework.core.convert.ConverterNotFoundException;
2831
import org.springframework.core.convert.converter.Converter;
2932
import org.springframework.data.convert.CustomConversions;
@@ -33,7 +36,12 @@
3336
import org.springframework.data.mapping.PersistentPropertyPath;
3437
import org.springframework.data.mapping.PreferredConstructor;
3538
import org.springframework.data.mapping.context.MappingContext;
39+
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
40+
import org.springframework.data.mapping.model.ParameterValueProvider;
3641
import org.springframework.data.mapping.model.SimpleTypeHolder;
42+
import org.springframework.data.mapping.model.SpELContext;
43+
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
44+
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
3745
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
3846
import org.springframework.data.relational.core.conversion.RelationalConverter;
3947
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
@@ -60,7 +68,7 @@
6068
* @see CustomConversions
6169
* @since 1.1
6270
*/
63-
public class BasicJdbcConverter extends BasicRelationalConverter implements JdbcConverter {
71+
public class BasicJdbcConverter extends BasicRelationalConverter implements JdbcConverter, ApplicationContextAware {
6472

6573
private static final Logger LOG = LoggerFactory.getLogger(BasicJdbcConverter.class);
6674
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
6977
private final IdentifierProcessing identifierProcessing;
7078

7179
private final RelationResolver relationResolver;
80+
private SpELContext spELContext;
7281

7382
/**
7483
* Creates a new {@link BasicRelationalConverter} given {@link MappingContext} and a
@@ -88,9 +97,10 @@ public BasicJdbcConverter(
8897

8998
Assert.notNull(relationResolver, "RelationResolver must not be null");
9099

91-
this.relationResolver = relationResolver;
92100
this.typeFactory = JdbcTypeFactory.unsupported();
93101
this.identifierProcessing = IdentifierProcessing.ANSI;
102+
this.relationResolver = relationResolver;
103+
this.spELContext = new SpELContext(ResultSetAccessorPropertyAccessor.INSTANCE);
94104
}
95105

96106
/**
@@ -113,9 +123,19 @@ public BasicJdbcConverter(
113123
Assert.notNull(relationResolver, "RelationResolver must not be null");
114124
Assert.notNull(identifierProcessing, "IdentifierProcessing must not be null");
115125

116-
this.relationResolver = relationResolver;
117126
this.typeFactory = typeFactory;
118127
this.identifierProcessing = identifierProcessing;
128+
this.relationResolver = relationResolver;
129+
this.spELContext = new SpELContext(ResultSetAccessorPropertyAccessor.INSTANCE);
130+
}
131+
132+
/*
133+
* (non-Javadoc)
134+
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
135+
*/
136+
@Override
137+
public void setApplicationContext(ApplicationContext applicationContext) {
138+
this.spELContext = new SpELContext(this.spELContext, applicationContext);
119139
}
120140

121141
@Nullable
@@ -344,11 +364,11 @@ private class ReadingContext<T> {
344364

345365
private final JdbcPropertyValueProvider propertyValueProvider;
346366
private final JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider;
367+
private final ResultSetAccessor accessor;
347368

348369
@SuppressWarnings("unchecked")
349370
private ReadingContext(PersistentPropertyPathExtension rootPath, ResultSetAccessor accessor, Identifier identifier,
350371
Object key) {
351-
352372
RelationalPersistentEntity<T> entity = (RelationalPersistentEntity<T>) rootPath.getLeafEntity();
353373

354374
Assert.notNull(entity, "The rootPath must point to an entity.");
@@ -361,26 +381,28 @@ private ReadingContext(PersistentPropertyPathExtension rootPath, ResultSetAccess
361381
this.propertyValueProvider = new JdbcPropertyValueProvider(identifierProcessing, path, accessor);
362382
this.backReferencePropertyValueProvider = new JdbcBackReferencePropertyValueProvider(identifierProcessing, path,
363383
accessor);
384+
this.accessor = accessor;
364385
}
365386

366387
private ReadingContext(RelationalPersistentEntity<T> entity, PersistentPropertyPathExtension rootPath,
367388
PersistentPropertyPathExtension path, Identifier identifier, Object key,
368389
JdbcPropertyValueProvider propertyValueProvider,
369-
JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider) {
390+
JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider, ResultSetAccessor accessor) {
370391
this.entity = entity;
371392
this.rootPath = rootPath;
372393
this.path = path;
373394
this.identifier = identifier;
374395
this.key = key;
375396
this.propertyValueProvider = propertyValueProvider;
376397
this.backReferencePropertyValueProvider = backReferencePropertyValueProvider;
398+
this.accessor = accessor;
377399
}
378400

379401
private <S> ReadingContext<S> extendBy(RelationalPersistentProperty property) {
380402
return new ReadingContext<>(
381403
(RelationalPersistentEntity<S>) getMappingContext().getRequiredPersistentEntity(property.getActualType()),
382404
rootPath.extendBy(property), path.extendBy(property), identifier, key,
383-
propertyValueProvider.extendBy(property), backReferencePropertyValueProvider.extendBy(property));
405+
propertyValueProvider.extendBy(property), backReferencePropertyValueProvider.extendBy(property), accessor);
384406
}
385407

386408
T mapRow() {
@@ -529,23 +551,70 @@ private Object readEntityFrom(RelationalPersistentProperty property) {
529551

530552
private T createInstanceInternal(@Nullable Object idValue) {
531553

532-
T instance = createInstance(entity, parameter -> {
554+
PreferredConstructor<T, RelationalPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
555+
ParameterValueProvider<RelationalPersistentProperty> provider;
533556

534-
String parameterName = parameter.getName();
557+
if (persistenceConstructor != null && persistenceConstructor.hasParameters()) {
535558

536-
Assert.notNull(parameterName, "A constructor parameter name must not be null to be used with Spring Data JDBC");
559+
SpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(accessor, spELContext);
560+
provider = new SpELExpressionParameterValueProvider<>(expressionEvaluator, getConversionService(),
561+
new ResultSetParameterValueProvider(idValue, entity));
562+
} else {
563+
provider = NoOpParameterValueProvider.INSTANCE;
564+
}
537565

538-
RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameterName);
539-
return readOrLoadProperty(idValue, property);
540-
});
566+
T instance = createInstance(entity, provider::getParameterValue);
541567

542568
return entity.requiresPropertyPopulation() ? populateProperties(instance, idValue) : instance;
543569
}
544570

571+
/**
572+
* {@link ParameterValueProvider} that reads a simple property or materializes an object for a
573+
* {@link RelationalPersistentProperty}.
574+
*
575+
* @see #readOrLoadProperty(Object, RelationalPersistentProperty)
576+
* @since 2.1
577+
*/
578+
private class ResultSetParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
579+
580+
private final @Nullable Object idValue;
581+
private final RelationalPersistentEntity<?> entity;
582+
583+
public ResultSetParameterValueProvider(@Nullable Object idValue, RelationalPersistentEntity<?> entity) {
584+
this.idValue = idValue;
585+
this.entity = entity;
586+
}
587+
588+
/*
589+
* (non-Javadoc)
590+
* @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter)
591+
*/
592+
@Override
593+
@Nullable
594+
public <T> T getParameterValue(PreferredConstructor.Parameter<T, RelationalPersistentProperty> parameter) {
595+
596+
String parameterName = parameter.getName();
597+
598+
Assert.notNull(parameterName, "A constructor parameter name must not be null to be used with Spring Data JDBC");
599+
600+
RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameterName);
601+
return (T) readOrLoadProperty(idValue, property);
602+
}
603+
}
545604
}
546605

547606
private boolean isSimpleProperty(RelationalPersistentProperty property) {
548607
return !property.isCollectionLike() && !property.isEntity() && !property.isMap() && !property.isEmbedded();
549608
}
550609

610+
enum NoOpParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
611+
612+
INSTANCE;
613+
614+
@Override
615+
public <T> T getParameterValue(PreferredConstructor.Parameter<T, RelationalPersistentProperty> parameter) {
616+
return null;
617+
}
618+
}
619+
551620
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core.convert;
17+
18+
import org.springframework.expression.EvaluationContext;
19+
import org.springframework.expression.PropertyAccessor;
20+
import org.springframework.expression.TypedValue;
21+
import org.springframework.lang.Nullable;
22+
23+
/**
24+
* {@link PropertyAccessor} to access a column from a {@link ResultSetAccessor}.
25+
*
26+
* @author Mark Paluch
27+
* @since 2.1
28+
*/
29+
class ResultSetAccessorPropertyAccessor implements PropertyAccessor {
30+
31+
static final PropertyAccessor INSTANCE = new ResultSetAccessorPropertyAccessor();
32+
33+
/*
34+
* (non-Javadoc)
35+
* @see org.springframework.expression.PropertyAccessor#getSpecificTargetClasses()
36+
*/
37+
@Override
38+
public Class<?>[] getSpecificTargetClasses() {
39+
return new Class<?>[] { ResultSetAccessor.class };
40+
}
41+
42+
/*
43+
* (non-Javadoc)
44+
* @see org.springframework.expression.PropertyAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
45+
*/
46+
@Override
47+
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
48+
return target instanceof ResultSetAccessor && ((ResultSetAccessor) target).hasValue(name);
49+
}
50+
51+
/*
52+
* (non-Javadoc)
53+
* @see org.springframework.expression.PropertyAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
54+
*/
55+
@Override
56+
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
57+
58+
if (target == null) {
59+
return TypedValue.NULL;
60+
}
61+
62+
Object value = ((ResultSetAccessor) target).getObject(name);
63+
64+
if (value == null) {
65+
return TypedValue.NULL;
66+
}
67+
68+
return new TypedValue(value);
69+
}
70+
71+
/*
72+
* (non-Javadoc)
73+
* @see org.springframework.expression.PropertyAccessor#canWrite(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
74+
*/
75+
@Override
76+
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) {
77+
return false;
78+
}
79+
80+
/*
81+
* (non-Javadoc)
82+
* @see org.springframework.expression.PropertyAccessor#write(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.lang.Object)
83+
*/
84+
@Override
85+
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) {
86+
throw new UnsupportedOperationException();
87+
}
88+
89+
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
import org.springframework.data.annotation.Id;
5353
import org.springframework.data.annotation.PersistenceConstructor;
54+
import org.springframework.data.annotation.Transient;
5455
import org.springframework.data.jdbc.core.mapping.AggregateReference;
5556
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
5657
import org.springframework.data.mapping.PersistentPropertyPath;
@@ -642,6 +643,19 @@ public void immutableOneToOneWithIdMissingColumnResultsInNullReference() throws
642643
assertThat(result.child).isNull();
643644
}
644645

646+
@Test // DATAJDBC-508
647+
public void materializesObjectWithAtValue() throws SQLException {
648+
649+
ResultSet rs = mockResultSet(asList("ID", "FIRST_NAME"), //
650+
123L, "Hello World");
651+
rs.next();
652+
653+
WithAtValue result = createRowMapper(WithAtValue.class).mapRow(rs, 1);
654+
655+
assertThat(result.getId()).isEqualTo(123L);
656+
assertThat(result.getComputed()).isEqualTo("Hello World");
657+
}
658+
645659
// Model classes to be used in tests
646660

647661
@With
@@ -1221,4 +1235,17 @@ private static class Expectation<T> {
12211235
final Object expectedValue;
12221236
final String sourceColumn;
12231237
}
1238+
1239+
@Getter
1240+
private static class WithAtValue {
1241+
1242+
@Id private final Long id;
1243+
private final @Transient String computed;
1244+
1245+
public WithAtValue(Long id,
1246+
@org.springframework.beans.factory.annotation.Value("#root.first_name") String computed) {
1247+
this.id = id;
1248+
this.computed = computed;
1249+
}
1250+
}
12241251
}

src/main/asciidoc/new-features.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This section covers the significant changes for each version.
77
== What's New in Spring Data JDBC 2.1
88

99
* Dialect for Oracle databases.
10+
* Support for `@Value` in persistence constructors.
1011

1112
[[new-features.2-0-0]]
1213
== What's New in Spring Data JDBC 2.0

0 commit comments

Comments
 (0)