Skip to content

Commit d7a7660

Browse files
committed
#450 - Add support for @value when constructing entities using their persistence constructor.
We now support the use of @value in persistence constructors to compute values for constructor creation. class MyDomainObject { public MyDomainObject(long id, @value("#root.my_column") String my_column, @value("5+2") int computed) { // … } }
1 parent a1081bb commit d7a7660

File tree

5 files changed

+148
-7
lines changed

5 files changed

+148
-7
lines changed

src/main/asciidoc/new-features.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Deprecate Spring Data R2DBC `DatabaseClient` and move off deprecated API in favor of Spring R2DBC. Consult the <<upgrading.1.1-1.2,Migration Guide>> for further details.
88
* Support for <<entity-callbacks>>.
99
* <<r2dbc.auditing,Auditing>> through `@EnableR2dbcAuditing`.
10+
* Support for `@Value` in persistence constructors.
1011

1112
[[new-features.1-1-0]]
1213
== What's New in Spring Data R2DBC 1.1.0

src/main/asciidoc/reference/mapping.adoc

+11-3
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,21 @@ Drivers can contribute additional simple types such as Geometry types.
158158
[[mapping.usage.annotations]]
159159
=== Mapping Annotation Overview
160160

161-
The `MappingR2dbcConverter` can use metadata to drive the mapping of objects to rows. The following annotations are available:
161+
The `MappingR2dbcConverter` can use metadata to drive the mapping of objects to rows.
162+
The following annotations are available:
162163

163164
* `@Id`: Applied at the field level to mark the primary key.
164165
* `@Table`: Applied at the class level to indicate this class is a candidate for mapping to the database.
165166
You can specify the name of the table where the database is stored.
166-
* `@Transient`: By default, all fields are mapped to the row. This annotation excludes the field where it is applied from being stored in the database. Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument.
167-
* `@PersistenceConstructor`: Marks a given constructor -- even a package protected one -- to use when instantiating the object from the database. Constructor arguments are mapped by name to the values in the retrieved row.
167+
* `@Transient`: By default, all fields are mapped to the row.
168+
This annotation excludes the field where it is applied from being stored in the database.
169+
Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument.
170+
* `@PersistenceConstructor`: Marks a given constructor -- even a package protected one -- to use when instantiating the object from the database.
171+
Constructor arguments are mapped by name to the values in the retrieved row.
172+
* `@Value`: This annotation is part of the Spring Framework.
173+
Within the mapping framework it can be applied to constructor arguments.
174+
This lets you use a Spring Expression Language statement to transform a key’s value retrieved in the database before it is used to construct a domain object.
175+
In order to reference a column of a given row one has to use expressions like: `@Value("#root.myProperty")` where root refers to the root of the given `Row`.
168176
* `@Column`: Applied at the field level to describe the name of the column as it is represented in the row, allowing the name to be different than the field name of the class.
169177

170178
The mapping metadata infrastructure is defined in the separate `spring-data-commons` project that is technology-agnostic.

src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java

+28-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535
import org.springframework.data.mapping.PreferredConstructor;
3636
import org.springframework.data.mapping.context.MappingContext;
3737
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
38+
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
3839
import org.springframework.data.mapping.model.ParameterValueProvider;
40+
import org.springframework.data.mapping.model.SpELContext;
41+
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
42+
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
3943
import org.springframework.data.r2dbc.mapping.OutboundRow;
4044
import org.springframework.data.r2dbc.support.ArrayUtils;
4145
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
@@ -294,10 +298,20 @@ private <S> S readEntityFrom(Row row, RowMetadata metadata, PersistentProperty<?
294298
private <S> S createInstance(Row row, @Nullable RowMetadata rowMetadata, String prefix,
295299
RelationalPersistentEntity<S> entity) {
296300

297-
RowParameterValueProvider rowParameterValueProvider = new RowParameterValueProvider(row, rowMetadata, entity, this,
298-
prefix);
301+
PreferredConstructor<S, RelationalPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
302+
ParameterValueProvider<RelationalPersistentProperty> provider;
299303

300-
return createInstance(entity, rowParameterValueProvider::getParameterValue);
304+
if (persistenceConstructor != null && persistenceConstructor.hasParameters()) {
305+
306+
SpELContext spELContext = new SpELContext(new RowPropertyAccessor(rowMetadata));
307+
SpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(row, spELContext);
308+
provider = new SpELExpressionParameterValueProvider<>(expressionEvaluator, getConversionService(),
309+
new RowParameterValueProvider(row, rowMetadata, entity, this, prefix));
310+
} else {
311+
provider = NoOpParameterValueProvider.INSTANCE;
312+
}
313+
314+
return createInstance(entity, provider::getParameterValue);
301315
}
302316

303317
// ----------------------------------
@@ -381,7 +395,7 @@ private boolean shouldSkipIdValue(@Nullable Object value, RelationalPersistentPr
381395
return value == null;
382396
}
383397

384-
if (Number.class.isInstance(value)) {
398+
if (value instanceof Number) {
385399
return ((Number) value).longValue() == 0L;
386400
}
387401

@@ -646,6 +660,16 @@ private static Collection<?> asCollection(Object source) {
646660
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
647661
}
648662

663+
enum NoOpParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
664+
665+
INSTANCE;
666+
667+
@Override
668+
public <T> T getParameterValue(PreferredConstructor.Parameter<T, RelationalPersistentProperty> parameter) {
669+
return null;
670+
}
671+
}
672+
649673
private static class RowParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
650674

651675
private final Row resultSet;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2013-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.r2dbc.convert;
17+
18+
import io.r2dbc.spi.Row;
19+
import io.r2dbc.spi.RowMetadata;
20+
21+
import org.springframework.expression.EvaluationContext;
22+
import org.springframework.expression.PropertyAccessor;
23+
import org.springframework.expression.TypedValue;
24+
import org.springframework.lang.Nullable;
25+
26+
/**
27+
* {@link PropertyAccessor} to read values from a {@link Row}.
28+
*
29+
* @author Mark Paluch
30+
* @since 1.2
31+
*/
32+
class RowPropertyAccessor implements PropertyAccessor {
33+
34+
private final @Nullable RowMetadata rowMetadata;
35+
36+
RowPropertyAccessor(@Nullable RowMetadata rowMetadata) {
37+
this.rowMetadata = rowMetadata;
38+
}
39+
40+
@Override
41+
public Class<?>[] getSpecificTargetClasses() {
42+
return new Class<?>[] { Row.class };
43+
}
44+
45+
@Override
46+
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
47+
return rowMetadata != null && target != null && rowMetadata.getColumnNames().contains(name);
48+
}
49+
50+
@Override
51+
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
52+
53+
if (target == null) {
54+
return TypedValue.NULL;
55+
}
56+
57+
Object value = ((Row) target).get(name);
58+
59+
if (value == null) {
60+
return TypedValue.NULL;
61+
}
62+
63+
return new TypedValue(value);
64+
}
65+
66+
@Override
67+
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) {
68+
return false;
69+
}
70+
71+
@Override
72+
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) {
73+
throw new UnsupportedOperationException();
74+
}
75+
}

src/test/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverterUnitTests.java

+33
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import static org.mockito.Mockito.*;
2020

2121
import io.r2dbc.spi.Row;
22+
import io.r2dbc.spi.test.MockColumnMetadata;
23+
import io.r2dbc.spi.test.MockRow;
24+
import io.r2dbc.spi.test.MockRowMetadata;
2225
import lombok.AllArgsConstructor;
2326
import lombok.RequiredArgsConstructor;
2427

@@ -31,9 +34,11 @@
3134
import org.junit.Before;
3235
import org.junit.Test;
3336

37+
import org.springframework.beans.factory.annotation.Value;
3438
import org.springframework.core.convert.converter.Converter;
3539
import org.springframework.dao.InvalidDataAccessApiUsageException;
3640
import org.springframework.data.annotation.Id;
41+
import org.springframework.data.annotation.Transient;
3742
import org.springframework.data.convert.ReadingConverter;
3843
import org.springframework.data.convert.WritingConverter;
3944
import org.springframework.data.r2dbc.mapping.OutboundRow;
@@ -208,6 +213,21 @@ public void writeShouldWritePrimitiveIdIfValueIsNonZero() {
208213
assertThat(row).containsEntry(SqlIdentifier.unquoted("id"), Parameter.fromOrEmpty(1L, Long.TYPE));
209214
}
210215

216+
@Test // gh-59
217+
public void shouldEvaluateSpelExpression() {
218+
219+
MockRow row = MockRow.builder().identified("id", Object.class, 42).identified("world", Object.class, "No, universe")
220+
.build();
221+
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(MockColumnMetadata.builder().name("id").build())
222+
.columnMetadata(MockColumnMetadata.builder().name("world").build()).build();
223+
224+
WithSpelExpression result = converter.read(WithSpelExpression.class, row, metadata);
225+
226+
assertThat(result.id).isEqualTo(42);
227+
assertThat(result.hello).isNull();
228+
assertThat(result.world).isEqualTo("No, universe");
229+
}
230+
211231
@AllArgsConstructor
212232
static class Person {
213233
@Id String id;
@@ -312,4 +332,17 @@ public CustomConversionPerson convert(Row source) {
312332
return person;
313333
}
314334
}
335+
336+
static class WithSpelExpression {
337+
338+
private long id;
339+
@Transient String hello;
340+
@Transient String world;
341+
342+
public WithSpelExpression(long id, @Value("null") String hello, @Value("#root.world") String world) {
343+
this.id = id;
344+
this.hello = hello;
345+
this.world = world;
346+
}
347+
}
315348
}

0 commit comments

Comments
 (0)