From d7a76609b2eca1da964e16e2aa815e45d6b84d4f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 8 Sep 2020 10:30:30 +0200 Subject: [PATCH] #450 - Add support for @Value when constructing entities using their persistence constructor. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) { // … } } --- src/main/asciidoc/new-features.adoc | 1 + src/main/asciidoc/reference/mapping.adoc | 14 +++- .../r2dbc/convert/MappingR2dbcConverter.java | 32 +++++++- .../r2dbc/convert/RowPropertyAccessor.java | 75 +++++++++++++++++++ .../MappingR2dbcConverterUnitTests.java | 33 ++++++++ 5 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/convert/RowPropertyAccessor.java diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 0bf96c1..9612fd9 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,6 +7,7 @@ * Deprecate Spring Data R2DBC `DatabaseClient` and move off deprecated API in favor of Spring R2DBC. Consult the <> for further details. * Support for <>. * <> through `@EnableR2dbcAuditing`. +* Support for `@Value` in persistence constructors. [[new-features.1-1-0]] == What's New in Spring Data R2DBC 1.1.0 diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 40d7c91..66be86e 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -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 key’s 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. diff --git a/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java index 6e1256e..afe9d5e 100644 --- a/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java @@ -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 createInstance(Row row, @Nullable RowMetadata rowMetadata, String prefix, RelationalPersistentEntity entity) { - RowParameterValueProvider rowParameterValueProvider = new RowParameterValueProvider(row, rowMetadata, entity, this, - prefix); + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + ParameterValueProvider 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 { + + INSTANCE; + + @Override + public T getParameterValue(PreferredConstructor.Parameter parameter) { + return null; + } + } + private static class RowParameterValueProvider implements ParameterValueProvider { private final Row resultSet; diff --git a/src/main/java/org/springframework/data/r2dbc/convert/RowPropertyAccessor.java b/src/main/java/org/springframework/data/r2dbc/convert/RowPropertyAccessor.java new file mode 100644 index 0000000..e912ee2 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/convert/RowPropertyAccessor.java @@ -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(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverterUnitTests.java b/src/test/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverterUnitTests.java index e5f49bf..2fe7e79 100644 --- a/src/test/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverterUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverterUnitTests.java @@ -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; + } + } }