diff --git a/src/main/antora/modules/ROOT/pages/object-mapping.adoc b/src/main/antora/modules/ROOT/pages/object-mapping.adoc
index 5828faa89..aac7900d3 100644
--- a/src/main/antora/modules/ROOT/pages/object-mapping.adoc
+++ b/src/main/antora/modules/ROOT/pages/object-mapping.adoc
@@ -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 {
@@ -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;
@@ -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`.
diff --git a/src/main/java/org/springframework/data/annotation/PersistenceCreator.java b/src/main/java/org/springframework/data/annotation/PersistenceCreator.java
index 814f149ef..9e2a2dde5 100644
--- a/src/main/java/org/springframework/data/annotation/PersistenceCreator.java
+++ b/src/main/java/org/springframework/data/annotation/PersistenceCreator.java
@@ -22,6 +22,8 @@ import java.lang.annotation.Target;
/**
* 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
diff --git a/src/main/java/org/springframework/data/annotation/Transient.java b/src/main/java/org/springframework/data/annotation/Transient.java
index 7e2429754..4b19709ce 100644
--- a/src/main/java/org/springframework/data/annotation/Transient.java
+++ b/src/main/java/org/springframework/data/annotation/Transient.java
@@ -22,13 +22,20 @@ import java.lang.annotation.RetentionPolicy;
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.
+ *
+ * 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.
+ *
+ * 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 {
}
diff --git a/src/main/java/org/springframework/data/mapping/PersistentEntity.java b/src/main/java/org/springframework/data/mapping/PersistentEntity.java
index ed7661e9f..81dac1fc8 100644
--- a/src/main/java/org/springframework/data/mapping/PersistentEntity.java
+++ b/src/main/java/org/springframework/data/mapping/PersistentEntity.java
@@ -49,8 +49,8 @@ public interface PersistentEntity> extends It
*
* @return {@literal null} in case no suitable creation mechanism for automatic construction can be found. This
* usually indicates that the instantiation of the object of that persistent entity is done through either a
- * customer {@link org.springframework.data.mapping.model.EntityInstantiator} or handled by custom
- * conversion mechanisms entirely.
+ * customer {@link org.springframework.data.mapping.model.EntityInstantiator} or handled by custom conversion
+ * mechanisms entirely.
* @since 3.0
*/
@Nullable
@@ -110,8 +110,8 @@ public interface PersistentEntity> extends It
}
/**
- * Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property
- * is available on the entity.
+ * Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property is
+ * available on the entity.
*
* @return the version property of the {@link PersistentEntity}.
*/
@@ -119,8 +119,8 @@ public interface PersistentEntity> extends It
P getVersionProperty();
/**
- * Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property
- * is available on the entity.
+ * Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property is
+ * available on the entity.
*
* @return the version property of the {@link PersistentEntity}.
* @throws IllegalStateException if {@link PersistentEntity} does not define a {@literal version} property.
@@ -140,7 +140,7 @@ public interface PersistentEntity> extends It
/**
* Obtains a {@link PersistentProperty} instance by name.
*
- * @param name The name of the property. Can be {@literal null}.
+ * @param name the name of the property. Can be {@literal null}.
* @return the {@link PersistentProperty} or {@literal null} if it doesn't exist.
*/
@Nullable
@@ -186,6 +186,28 @@ public interface PersistentEntity> extends It
*/
Iterable
getPersistentProperties(Class extends Annotation> annotationType);
+ /**
+ * Obtains a transient {@link PersistentProperty} instance by name. You can check with {@link #isTransient(String)}
+ * whether there is a transient property before calling this method.
+ *
+ * @param name the name of the property. Can be {@literal null}.
+ * @return the {@link PersistentProperty} or {@literal null} if it doesn't exist.
+ * @since 3.3
+ * @see #isTransient(String)
+ */
+ @Nullable
+ P getTransientProperty(String name);
+
+ /**
+ * Returns whether the property is transient.
+ *
+ * @param property name of the property.
+ * @return {@code true} if the property is transient. Applies only for existing properties. {@code false} if the
+ * property does not exist or is not transient.
+ * @since 3.3
+ */
+ boolean isTransient(String property);
+
/**
* Returns whether the {@link PersistentEntity} has an id property. If this call returns {@literal true},
* {@link #getIdProperty()} will return a non-{@literal null} value.
@@ -210,8 +232,8 @@ public interface PersistentEntity> extends It
Class getType();
/**
- * Returns the alias to be used when storing type information. Might be {@literal null} to indicate that there was
- * no alias defined through the mapping metadata.
+ * Returns the alias to be used when storing type information. Might be {@literal null} to indicate that there was no
+ * alias defined through the mapping metadata.
*
* @return
*/
@@ -241,8 +263,8 @@ public interface PersistentEntity> extends It
void doWithProperties(SimplePropertyHandler handler);
/**
- * Applies the given {@link AssociationHandler} to all {@link Association} contained in this
- * {@link PersistentEntity}. The iteration order is undefined.
+ * Applies the given {@link AssociationHandler} to all {@link Association} contained in this {@link PersistentEntity}.
+ * The iteration order is undefined.
*
* @param handler must not be {@literal null}.
*/
@@ -257,8 +279,8 @@ public interface PersistentEntity> extends It
void doWithAssociations(SimpleAssociationHandler handler);
/**
- * Applies the given {@link PropertyHandler} to both all {@link PersistentProperty}s as well as all inverse
- * properties of all {@link Association}s. The iteration order is undefined.
+ * Applies the given {@link PropertyHandler} to both all {@link PersistentProperty}s as well as all inverse properties
+ * of all {@link Association}s. The iteration order is undefined.
*
* @param handler must not be {@literal null}.
* @since 2.5
@@ -342,7 +364,7 @@ public interface PersistentEntity> extends It
*
* @param bean must not be {@literal null}.
* @throws IllegalArgumentException in case the given bean is not an instance of the typ represented by the
- * {@link PersistentEntity}.
+ * {@link PersistentEntity}.
* @return whether the given bean is considered a new instance.
*/
boolean isNew(Object bean);
@@ -358,8 +380,8 @@ public interface PersistentEntity> extends It
boolean isImmutable();
/**
- * Returns whether the entity needs properties to be populated, i.e. if any property exists that's not initialized
- * by the constructor.
+ * Returns whether the entity needs properties to be populated, i.e. if any property exists that's not initialized by
+ * the constructor.
*
* @return
* @since 2.1
diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
index 38febd4bf..177ad9011 100644
--- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
+++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
@@ -646,10 +646,6 @@ public abstract class AbstractMappingContext>
private final @Nullable InstanceCreatorMetadata
creator;
private final TypeInformation information;
private final List
properties;
+ private final List
transientProperties;
private final List
persistentPropertiesCache;
private final @Nullable Comparator
comparator;
private final Set> associations;
private final Map propertyCache;
+
+ private final Map transientPropertyCache;
private final Map, Optional> annotationCache;
private final MultiValueMap, P> propertyAnnotationCache;
@@ -114,12 +117,14 @@ public class BasicPersistentEntity>
this.information = information;
this.properties = new ArrayList<>();
+ this.transientProperties = new ArrayList<>(0);
this.persistentPropertiesCache = new ArrayList<>();
this.comparator = comparator;
this.creator = InstanceCreatorMetadataDiscoverer.discover(this);
this.associations = comparator == null ? new HashSet<>() : new TreeSet<>(new AssociationComparator<>(comparator));
this.propertyCache = new HashMap<>(16, 1.0f);
+ this.transientPropertyCache = new HashMap<>(0, 1f);
this.annotationCache = new ConcurrentHashMap<>(16);
this.propertyAnnotationCache = CollectionUtils.toMultiValueMap(new ConcurrentHashMap<>(16));
this.propertyAccessorFactory = BeanWrapperPropertyAccessorFactory.INSTANCE;
@@ -186,6 +191,18 @@ public class BasicPersistentEntity>
Assert.notNull(property, "Property must not be null");
+ if (property.isTransient()) {
+
+ if (transientProperties.contains(property)) {
+ return;
+ }
+
+ transientProperties.add(property);
+ transientPropertyCache.put(property.getName(), property);
+
+ return;
+ }
+
if (properties.contains(property)) {
return;
}
@@ -279,6 +296,19 @@ public class BasicPersistentEntity>
return propertyCache.get(name);
}
+ @Override
+ public P getTransientProperty(String name) {
+ return transientPropertyCache.get(name);
+ }
+
+ @Override
+ public boolean isTransient(String property) {
+
+ P transientProperty = getTransientProperty(property);
+
+ return transientProperty != null && transientProperty.isTransient();
+ }
+
@Override
public Iterable
getPersistentProperties(Class extends Annotation> annotationType) {
diff --git a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java
index c0a3a824a..7ba7942dd 100644
--- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java
+++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java
@@ -17,11 +17,13 @@ package org.springframework.data.mapping.model;
import org.jspecify.annotations.Nullable;
+import org.springframework.data.annotation.Transient;
import org.springframework.data.mapping.InstanceCreatorMetadata;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.util.ReflectionUtils;
/**
* {@link ParameterValueProvider} based on a {@link PersistentEntity} to use a {@link PropertyValueProvider} to lookup
@@ -29,6 +31,7 @@ import org.springframework.data.mapping.PersistentProperty;
*
* @author Oliver Gierke
* @author Johannes Englmeier
+ * @author Mark Paluch
*/
public class PersistentEntityParameterValueProvider
>
implements ParameterValueProvider
{
@@ -45,17 +48,25 @@ public class PersistentEntityParameterValueProvider
parameterType) {
+ return parameterType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(parameterType) : null;
+ }
+
@Nullable
@SuppressWarnings("unchecked")
public T getParameterValue(Parameter parameter) {
InstanceCreatorMetadata
creator = entity.getInstanceCreatorMetadata();
+ String name = parameter.getName();
if (creator != null && creator.isParentParameter(parameter)) {
return (T) parent;
}
- String name = parameter.getName();
+ if (parameter.getAnnotations().isPresent(Transient.class) || (name != null && entity.isTransient(name))) {
+ return (T) getTransientDefault(parameter.getRawType());
+ }
if (name == null) {
throw new MappingException(String.format("Parameter %s does not have a name", parameter));
diff --git a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java
index c3c3e03d0..52ee42285 100755
--- a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java
@@ -32,6 +32,8 @@ import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -106,8 +108,7 @@ class BasicPersistentEntityUnitTests> {
@SuppressWarnings("unchecked")
void considersComparatorForPropertyOrder() {
- var entity = createEntity(Person.class,
- Comparator.comparing(PersistentProperty::getName));
+ var entity = createEntity(Person.class, Comparator.comparing(PersistentProperty::getName));
var lastName = (T) Mockito.mock(PersistentProperty.class);
when(lastName.getName()).thenReturn("lastName");
@@ -199,8 +200,8 @@ class BasicPersistentEntityUnitTests> {
assertThat(accessor).isNotInstanceOf(BeanWrapper.class);
assertThat(accessor).isInstanceOfSatisfying(InstantiationAwarePropertyAccessor.class, it -> {
- var delegateFunction = (Function