Skip to content

Commit 9b05305

Browse files
committed
Ignore @Transient properties in constructors.
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.
1 parent d43913f commit 9b05305

File tree

5 files changed

+128
-17
lines changed

5 files changed

+128
-17
lines changed

src/main/antora/modules/ROOT/pages/object-mapping.adoc

+8-4
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ By default, Spring Data attempts to use generated property accessors and falls b
154154
Let's have a look at the following entity:
155155

156156
.A sample entity
157-
[source, java]
157+
[source,java]
158158
----
159159
class Person {
160160
@@ -165,14 +165,15 @@ class Person {
165165
166166
private String comment; <4>
167167
private @AccessType(Type.PROPERTY) String remarks; <5>
168+
private @Transient String summary; <6>
168169
169-
static Person of(String firstname, String lastname, LocalDate birthday) { <6>
170+
static Person of(String firstname, String lastname, LocalDate birthday) { <7>
170171
171172
return new Person(null, firstname, lastname, birthday,
172173
Period.between(birthday, LocalDate.now()).getYears());
173174
}
174175
175-
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { <6>
176+
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { <7>
176177
177178
this.id = id;
178179
this.firstname = firstname;
@@ -201,7 +202,10 @@ With the design shown, the database value will trump the defaulting as Spring Da
201202
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.
202203
<4> The `comment` property is mutable and is populated by setting its field directly.
203204
<5> The `remarks` property is mutable and is populated by invoking the setter method.
204-
<6> The class exposes a factory method and a constructor for object creation.
205+
<6> The `summary` property transient and will not be persisted as it is annotated with `@Transient`.
206+
Spring Data doesn't use Java's `transient` keyword to exclude properties from being persisted as `transient` is part of the Java Serialization Framework.
207+
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).
208+
<7> The class exposes a factory method and a constructor for object creation.
205209
The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through `@PersistenceCreator`.
206210
Instead, defaulting of properties is handled within the factory method.
207211
If you want Spring Data to use the factory method for object instantiation, annotate it with `@PersistenceCreator`.

src/main/java/org/springframework/data/annotation/PersistenceCreator.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222

2323
/**
2424
* Marker annotation to declare a constructor or factory method annotation as factory/preferred constructor annotation.
25+
* Properties used by the constructor (or factory method) must refer to persistent properties or be annotated with
26+
* {@link org.springframework.beans.factory.annotation.Value @Value(…)} to obtain a value for object creation.
2527
*
2628
* @author Mark Paluch
2729
* @author Oliver Drotbohm
2830
* @since 3.0
2931
*/
3032
@Retention(RetentionPolicy.RUNTIME)
3133
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
32-
public @interface PersistenceCreator {}
34+
public @interface PersistenceCreator {
35+
}

src/main/java/org/springframework/data/annotation/Transient.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,20 @@
2222
import java.lang.annotation.Target;
2323

2424
/**
25-
* Marks a field to be transient for the mapping framework. Thus the property will not be persisted and not further
26-
* inspected by the mapping framework.
25+
* Marks a field to be transient for the mapping framework. Thus, the property will not be persisted.
26+
* <p>
27+
* Excluding properties from the persistence mechanism is separate from Java's {@code transient} keyword that serves the
28+
* purpose of excluding properties from being serialized through Java Serialization.
29+
* <p>
30+
* Transient properties can be used in {@link PersistenceCreator constructor creation/factory methods}, however they
31+
* will use Java default values. We highly recommend using {@link org.springframework.beans.factory.annotation.Value
32+
* SpEL expressions through @Value(…)} to provide a meaningful value.
2733
*
2834
* @author Oliver Gierke
2935
* @author Jon Brisbin
36+
* @author Mark Paluch
3037
*/
3138
@Retention(RetentionPolicy.RUNTIME)
32-
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
39+
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE, RECORD_COMPONENT })
3340
public @interface Transient {
3441
}

src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java

+12-9
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@
2121
import org.springframework.data.mapping.Parameter;
2222
import org.springframework.data.mapping.PersistentEntity;
2323
import org.springframework.data.mapping.PersistentProperty;
24+
import org.springframework.data.util.ReflectionUtils;
2425
import org.springframework.lang.Nullable;
2526

2627
/**
27-
* {@link ParameterValueProvider} based on a {@link PersistentEntity} to use a {@link PropertyValueProvider} to lookup
28-
* the value of the property referenced by the given {@link Parameter}. Additionally a
28+
* {@link ParameterValueProvider} based on a {@link PersistentEntity} to use a {@link PropertyValueProvider} to look up
29+
* the value of the property referenced by the given {@link Parameter}. Additionally, a
2930
* {@link DefaultSpELExpressionEvaluator} can be configured to get property value resolution trumped by a SpEL
3031
* expression evaluation.
3132
*
3233
* @author Oliver Gierke
3334
* @author Johannes Englmeier
35+
* @author Mark Paluch
3436
*/
3537
public class PersistentEntityParameterValueProvider<P extends PersistentProperty<P>>
3638
implements ParameterValueProvider<P> {
@@ -46,25 +48,26 @@ public PersistentEntityParameterValueProvider(PersistentEntity<?, P> entity, Pro
4648
this.parent = parent;
4749
}
4850

51+
@Nullable
52+
private static Object getTransientDefault(Class<?> parameterType) {
53+
return parameterType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(parameterType) : null;
54+
}
55+
4956
@Nullable
5057
@SuppressWarnings("unchecked")
5158
public <T> T getParameterValue(Parameter<T, P> parameter) {
5259

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

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

59-
if (parameter.getAnnotations().isPresent(Transient.class)) {
60-
61-
// parameter.getRawType().isPrimitive()
62-
return null;
63-
67+
if (parameter.getAnnotations().isPresent(Transient.class) || (name != null && entity.isTransient(name))) {
68+
return (T) getTransientDefault(parameter.getRawType());
6469
}
6570

66-
String name = parameter.getName();
67-
6871
if (name == null) {
6972
throw new MappingException(String.format("Parameter %s does not have a name", parameter));
7073
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2023 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.mapping.model;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.springframework.data.annotation.Transient;
22+
import org.springframework.data.mapping.context.SampleMappingContext;
23+
import org.springframework.data.mapping.context.SamplePersistentProperty;
24+
25+
/**
26+
* Integration tests for {@link EntityInstantiator}.
27+
*
28+
* @author Mark Paluch
29+
*/
30+
public class EntityInstantiatorIntegrationTests {
31+
32+
SampleMappingContext context = new SampleMappingContext();
33+
EntityInstantiators instantiators = new EntityInstantiators();
34+
35+
@Test // GH-2942
36+
void shouldDefaultTransientProperties() {
37+
38+
WithTransientProperty instance = createInstance(WithTransientProperty.class);
39+
40+
assertThat(instance.foo).isEqualTo(null);
41+
assertThat(instance.bar).isEqualTo(0);
42+
}
43+
44+
@Test // GH-2942
45+
void shouldDefaultTransientRecordProperties() {
46+
47+
RecordWithTransientProperty instance = createInstance(RecordWithTransientProperty.class);
48+
49+
assertThat(instance.foo).isEqualTo(null);
50+
assertThat(instance.bar).isEqualTo(0);
51+
}
52+
53+
@Test // GH-2942
54+
void shouldDefaultTransientKotlinProperty() {
55+
56+
DataClassWithTransientProperties instance = createInstance(DataClassWithTransientProperties.class);
57+
58+
// Kotlin defaulting
59+
assertThat(instance.getFoo()).isEqualTo("foo");
60+
61+
// Our defaulting
62+
assertThat(instance.getBar()).isEqualTo(0);
63+
}
64+
65+
@SuppressWarnings("unchecked")
66+
private <E> E createInstance(Class<E> entityType) {
67+
68+
var entity = context.getRequiredPersistentEntity(entityType);
69+
var instantiator = instantiators.getInstantiatorFor(entity);
70+
71+
return (E) instantiator.createInstance(entity,
72+
new PersistentEntityParameterValueProvider<>(entity, new PropertyValueProvider<SamplePersistentProperty>() {
73+
@Override
74+
public <T> T getPropertyValue(SamplePersistentProperty property) {
75+
return null;
76+
}
77+
}, null));
78+
}
79+
80+
static class WithTransientProperty {
81+
82+
@Transient String foo;
83+
@Transient int bar;
84+
85+
public WithTransientProperty(String foo, int bar) {
86+
87+
}
88+
}
89+
90+
record RecordWithTransientProperty(@Transient String foo, @Transient int bar) {
91+
92+
}
93+
94+
}

0 commit comments

Comments
 (0)