Skip to content

Commit

Permalink
Ignore @Transient properties in constructors.
Browse files Browse the repository at this point in the history
We now ignore transient properties used in constructors. Regular transient properties default to the Java default values (null for object types, 0 for numeric primitives and so on).

Using transient properties allows leveraging Kotlin's defaulting mechanism to infer default values.

Record components can also be annotated with the Transient annotation to allow record construction. While this can be useful, we recommend using the Value annotation to use SpEL expressions to determine a useful value.
  • Loading branch information
mp911de committed Nov 21, 2023
1 parent d43913f commit 9b05305
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 17 deletions.
12 changes: 8 additions & 4 deletions src/main/antora/modules/ROOT/pages/object-mapping.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ By default, Spring Data attempts to use generated property accessors and falls b
Let's have a look at the following entity:

.A sample entity
[source, java]
[source,java]
----
class Person {
Expand All @@ -165,14 +165,15 @@ class Person {
private String comment; <4>
private @AccessType(Type.PROPERTY) String remarks; <5>
private @Transient String summary; <6>
static Person of(String firstname, String lastname, LocalDate birthday) { <6>
static Person of(String firstname, String lastname, LocalDate birthday) { <7>
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { <6>
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { <7>
this.id = id;
this.firstname = firstname;
Expand Down Expand Up @@ -201,7 +202,10 @@ With the design shown, the database value will trump the defaulting as Spring Da
Even if the intent is that the calculation should be preferred, it's important that this constructor also takes `age` as parameter (to potentially ignore it) as otherwise the property population step will attempt to set the age field and fail due to it being immutable and no `with…` method being present.
<4> The `comment` property is mutable and is populated by setting its field directly.
<5> The `remarks` property is mutable and is populated by invoking the setter method.
<6> The class exposes a factory method and a constructor for object creation.
<6> The `summary` property transient and will not be persisted as it is annotated with `@Transient`.
Spring Data doesn't use Java's `transient` keyword to exclude properties from being persisted as `transient` is part of the Java Serialization Framework.
Note that this property can be used with a persistence constructor, but its value will default to `null` (or the respective primitive initial value if the property type was a primitive one).
<7> The class exposes a factory method and a constructor for object creation.
The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through `@PersistenceCreator`.
Instead, defaulting of properties is handled within the factory method.
If you want Spring Data to use the factory method for object instantiation, annotate it with `@PersistenceCreator`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@

/**
* Marker annotation to declare a constructor or factory method annotation as factory/preferred constructor annotation.
* Properties used by the constructor (or factory method) must refer to persistent properties or be annotated with
* {@link org.springframework.beans.factory.annotation.Value @Value(…)} to obtain a value for object creation.
*
* @author Mark Paluch
* @author Oliver Drotbohm
* @since 3.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface PersistenceCreator {}
public @interface PersistenceCreator {
}
13 changes: 10 additions & 3 deletions src/main/java/org/springframework/data/annotation/Transient.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@
import java.lang.annotation.Target;

/**
* Marks a field to be transient for the mapping framework. Thus the property will not be persisted and not further
* inspected by the mapping framework.
* Marks a field to be transient for the mapping framework. Thus, the property will not be persisted.
* <p>
* Excluding properties from the persistence mechanism is separate from Java's {@code transient} keyword that serves the
* purpose of excluding properties from being serialized through Java Serialization.
* <p>
* Transient properties can be used in {@link PersistenceCreator constructor creation/factory methods}, however they
* will use Java default values. We highly recommend using {@link org.springframework.beans.factory.annotation.Value
* SpEL expressions through @Value(…)} to provide a meaningful value.
*
* @author Oliver Gierke
* @author Jon Brisbin
* @author Mark Paluch
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE, RECORD_COMPONENT })
public @interface Transient {
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.lang.Nullable;

/**
* {@link ParameterValueProvider} based on a {@link PersistentEntity} to use a {@link PropertyValueProvider} to lookup
* the value of the property referenced by the given {@link Parameter}. Additionally a
* {@link ParameterValueProvider} based on a {@link PersistentEntity} to use a {@link PropertyValueProvider} to look up
* the value of the property referenced by the given {@link Parameter}. Additionally, a
* {@link DefaultSpELExpressionEvaluator} can be configured to get property value resolution trumped by a SpEL
* expression evaluation.
*
* @author Oliver Gierke
* @author Johannes Englmeier
* @author Mark Paluch
*/
public class PersistentEntityParameterValueProvider<P extends PersistentProperty<P>>
implements ParameterValueProvider<P> {
Expand All @@ -46,25 +48,26 @@ public PersistentEntityParameterValueProvider(PersistentEntity<?, P> entity, Pro
this.parent = parent;
}

@Nullable
private static Object getTransientDefault(Class<?> parameterType) {
return parameterType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(parameterType) : null;
}

@Nullable
@SuppressWarnings("unchecked")
public <T> T getParameterValue(Parameter<T, P> parameter) {

InstanceCreatorMetadata<P> creator = entity.getInstanceCreatorMetadata();
String name = parameter.getName();

if (creator != null && creator.isParentParameter(parameter)) {
return (T) parent;
}

if (parameter.getAnnotations().isPresent(Transient.class)) {

// parameter.getRawType().isPrimitive()
return null;

if (parameter.getAnnotations().isPresent(Transient.class) || (name != null && entity.isTransient(name))) {
return (T) getTransientDefault(parameter.getRawType());
}

String name = parameter.getName();

if (name == null) {
throw new MappingException(String.format("Parameter %s does not have a name", parameter));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2023 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.mapping.model;

import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.mapping.context.SamplePersistentProperty;

/**
* Integration tests for {@link EntityInstantiator}.
*
* @author Mark Paluch
*/
public class EntityInstantiatorIntegrationTests {

SampleMappingContext context = new SampleMappingContext();
EntityInstantiators instantiators = new EntityInstantiators();

@Test // GH-2942
void shouldDefaultTransientProperties() {

WithTransientProperty instance = createInstance(WithTransientProperty.class);

assertThat(instance.foo).isEqualTo(null);
assertThat(instance.bar).isEqualTo(0);
}

@Test // GH-2942
void shouldDefaultTransientRecordProperties() {

RecordWithTransientProperty instance = createInstance(RecordWithTransientProperty.class);

assertThat(instance.foo).isEqualTo(null);
assertThat(instance.bar).isEqualTo(0);
}

@Test // GH-2942
void shouldDefaultTransientKotlinProperty() {

DataClassWithTransientProperties instance = createInstance(DataClassWithTransientProperties.class);

// Kotlin defaulting
assertThat(instance.getFoo()).isEqualTo("foo");

// Our defaulting
assertThat(instance.getBar()).isEqualTo(0);
}

@SuppressWarnings("unchecked")
private <E> E createInstance(Class<E> entityType) {

var entity = context.getRequiredPersistentEntity(entityType);
var instantiator = instantiators.getInstantiatorFor(entity);

return (E) instantiator.createInstance(entity,
new PersistentEntityParameterValueProvider<>(entity, new PropertyValueProvider<SamplePersistentProperty>() {
@Override
public <T> T getPropertyValue(SamplePersistentProperty property) {
return null;
}
}, null));
}

static class WithTransientProperty {

@Transient String foo;
@Transient int bar;

public WithTransientProperty(String foo, int bar) {

}
}

record RecordWithTransientProperty(@Transient String foo, @Transient int bar) {

}

}

0 comments on commit 9b05305

Please sign in to comment.