#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) {
		// …
	}
}
This commit is contained in:
Mark Paluch
2020-09-08 10:30:30 +02:00
parent a1081bbbb2
commit d7a76609b2
5 changed files with 148 additions and 7 deletions

View File

@@ -7,6 +7,7 @@
* 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.
* Support for <<entity-callbacks>>.
* <<r2dbc.auditing,Auditing>> through `@EnableR2dbcAuditing`.
* Support for `@Value` in persistence constructors.
[[new-features.1-1-0]]
== What's New in Spring Data R2DBC 1.1.0

View File

@@ -158,13 +158,21 @@ Drivers can contribute additional simple types such as Geometry types.
[[mapping.usage.annotations]]
=== Mapping Annotation Overview
The `MappingR2dbcConverter` can use metadata to drive the mapping of objects to rows. The following annotations are available:
The `MappingR2dbcConverter` can use metadata to drive the mapping of objects to rows.
The following annotations are available:
* `@Id`: Applied at the field level to mark the primary key.
* `@Table`: Applied at the class level to indicate this class is a candidate for mapping to the database.
You can specify the name of the table where the database is stored.
* `@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.
* `@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.
* `@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.
* `@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.
* `@Value`: This annotation is part of the Spring Framework.
Within the mapping framework it can be applied to constructor arguments.
This lets you use a Spring Expression Language statement to transform a keys value retrieved in the database before it is used to construct a domain object.
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`.
* `@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.
The mapping metadata infrastructure is defined in the separate `spring-data-commons` project that is technology-agnostic.

View File

@@ -35,7 +35,11 @@ import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
import org.springframework.data.mapping.model.ParameterValueProvider;
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.r2dbc.mapping.OutboundRow;
import org.springframework.data.r2dbc.support.ArrayUtils;
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
@@ -294,10 +298,20 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
private <S> S createInstance(Row row, @Nullable RowMetadata rowMetadata, String prefix,
RelationalPersistentEntity<S> entity) {
RowParameterValueProvider rowParameterValueProvider = new RowParameterValueProvider(row, rowMetadata, entity, this,
prefix);
PreferredConstructor<S, RelationalPersistentProperty> persistenceConstructor = entity.getPersistenceConstructor();
ParameterValueProvider<RelationalPersistentProperty> provider;
return createInstance(entity, rowParameterValueProvider::getParameterValue);
if (persistenceConstructor != null && persistenceConstructor.hasParameters()) {
SpELContext spELContext = new SpELContext(new RowPropertyAccessor(rowMetadata));
SpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(row, spELContext);
provider = new SpELExpressionParameterValueProvider<>(expressionEvaluator, getConversionService(),
new RowParameterValueProvider(row, rowMetadata, entity, this, prefix));
} else {
provider = NoOpParameterValueProvider.INSTANCE;
}
return createInstance(entity, provider::getParameterValue);
}
// ----------------------------------
@@ -381,7 +395,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
return value == null;
}
if (Number.class.isInstance(value)) {
if (value instanceof Number) {
return ((Number) value).longValue() == 0L;
}
@@ -646,6 +660,16 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
}
enum NoOpParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
INSTANCE;
@Override
public <T> T getParameterValue(PreferredConstructor.Parameter<T, RelationalPersistentProperty> parameter) {
return null;
}
}
private static class RowParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
private final Row resultSet;

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013-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.r2dbc.convert;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.lang.Nullable;
/**
* {@link PropertyAccessor} to read values from a {@link Row}.
*
* @author Mark Paluch
* @since 1.2
*/
class RowPropertyAccessor implements PropertyAccessor {
private final @Nullable RowMetadata rowMetadata;
RowPropertyAccessor(@Nullable RowMetadata rowMetadata) {
this.rowMetadata = rowMetadata;
}
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class<?>[] { Row.class };
}
@Override
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
return rowMetadata != null && target != null && rowMetadata.getColumnNames().contains(name);
}
@Override
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
if (target == null) {
return TypedValue.NULL;
}
Object value = ((Row) target).get(name);
if (value == null) {
return TypedValue.NULL;
}
return new TypedValue(value);
}
@Override
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) {
return false;
}
@Override
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) {
throw new UnsupportedOperationException();
}
}

View File

@@ -19,6 +19,9 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.test.MockColumnMetadata;
import io.r2dbc.spi.test.MockRow;
import io.r2dbc.spi.test.MockRowMetadata;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
@@ -31,9 +34,11 @@ import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.r2dbc.mapping.OutboundRow;
@@ -208,6 +213,21 @@ public class MappingR2dbcConverterUnitTests {
assertThat(row).containsEntry(SqlIdentifier.unquoted("id"), Parameter.fromOrEmpty(1L, Long.TYPE));
}
@Test // gh-59
public void shouldEvaluateSpelExpression() {
MockRow row = MockRow.builder().identified("id", Object.class, 42).identified("world", Object.class, "No, universe")
.build();
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(MockColumnMetadata.builder().name("id").build())
.columnMetadata(MockColumnMetadata.builder().name("world").build()).build();
WithSpelExpression result = converter.read(WithSpelExpression.class, row, metadata);
assertThat(result.id).isEqualTo(42);
assertThat(result.hello).isNull();
assertThat(result.world).isEqualTo("No, universe");
}
@AllArgsConstructor
static class Person {
@Id String id;
@@ -312,4 +332,17 @@ public class MappingR2dbcConverterUnitTests {
return person;
}
}
static class WithSpelExpression {
private long id;
@Transient String hello;
@Transient String world;
public WithSpelExpression(long id, @Value("null") String hello, @Value("#root.world") String world) {
this.id = id;
this.hello = hello;
this.world = world;
}
}
}