diff --git a/pom.xml b/pom.xml index 9104c8adb6..d028c3409d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.1.0-SNAPSHOT + 4.1.0-ISSUE-1432-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. diff --git a/src/main/antora/modules/ROOT/pages/object-mapping.adoc b/src/main/antora/modules/ROOT/pages/object-mapping.adoc index 5828faa898..aac7900d37 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 814f149ef8..9e2a2dde56 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 @@ /** * 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 7e24297544..4b19709ce6 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.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/auditing/AuditingHandlerSupport.java b/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java index 17dc140fad..4ebd3211e7 100644 --- a/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java +++ b/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java @@ -61,7 +61,7 @@ public AuditingHandlerSupport(PersistentEntities entities) { /** * Setter do determine if {@link Auditable#setCreatedDate(TemporalAccessor)}} and * {@link Auditable#setLastModifiedDate(TemporalAccessor)} shall be filled with the current Java time. Defaults to - * {@code true}. One might set this to {@code false} to use database features to set entity time. + * {@literal true}. One might set this to {@literal false} to use database features to set entity time. * * @param dateTimeForNow the dateTimeForNow to set */ @@ -71,7 +71,7 @@ public void setDateTimeForNow(boolean dateTimeForNow) { /** * Set this to true if you want to treat entity creation as modification and thus setting the current date as - * modification date during creation, too. Defaults to {@code true}. + * modification date during creation, too. Defaults to {@literal true}. * * @param modifyOnCreation if modification information shall be set on creation, too */ diff --git a/src/main/java/org/springframework/data/convert/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/convert/DtoInstantiatingConverter.java index 0e7aae219b..720f585905 100644 --- a/src/main/java/org/springframework/data/convert/DtoInstantiatingConverter.java +++ b/src/main/java/org/springframework/data/convert/DtoInstantiatingConverter.java @@ -16,7 +16,6 @@ package org.springframework.data.convert; import org.jspecify.annotations.Nullable; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.mapping.InstanceCreatorMetadata; import org.springframework.data.mapping.Parameter; @@ -37,6 +36,7 @@ * * @author Mark Paluch * @author Oliver Drotbohm + * @author Christoph Strobl * @since 2.7 */ public class DtoInstantiatingConverter implements Converter { @@ -91,6 +91,10 @@ public Object convert(Object source) { throw new IllegalArgumentException(String.format("Parameter %s does not have a name", parameter)); } + if (parameter.isTransient()) { + return ParameterValueProvider.getDefaultValue(parameter.getRawType()); + } + return sourceAccessor.getProperty(sourceEntity.getRequiredPersistentProperty(name)); } }); diff --git a/src/main/java/org/springframework/data/domain/Window.java b/src/main/java/org/springframework/data/domain/Window.java index 197fd70ea2..00bbef0dfd 100644 --- a/src/main/java/org/springframework/data/domain/Window.java +++ b/src/main/java/org/springframework/data/domain/Window.java @@ -66,9 +66,9 @@ static Window from(List items, IntFunction p int size(); /** - * Returns {@code true} if this window contains no elements. + * Returns {@literal true} if this window contains no elements. * - * @return {@code true} if this window contains no elements + * @return {@literal true} if this window contains no elements */ @Override boolean isEmpty(); @@ -100,7 +100,7 @@ default boolean isLast() { * Returns whether the underlying scroll mechanism can provide a {@link ScrollPosition} at {@code index}. * * @param index the result index to check for a {@link ScrollPosition}. - * @return {@code true} if a {@link ScrollPosition} can be created; {@code false} otherwise. + * @return {@literal true} if a {@link ScrollPosition} can be created; {@literal false} otherwise. * @see #positionAt(int) */ default boolean hasPosition(int index) { diff --git a/src/main/java/org/springframework/data/expression/ValueExpression.java b/src/main/java/org/springframework/data/expression/ValueExpression.java index 64f56d9060..e7164b16e9 100644 --- a/src/main/java/org/springframework/data/expression/ValueExpression.java +++ b/src/main/java/org/springframework/data/expression/ValueExpression.java @@ -49,7 +49,7 @@ default ExpressionDependencies getExpressionDependencies() { /** * Returns whether the expression is a literal expression (that doesn't actually require evaluation). * - * @return {@code true} if the expression is a literal expression; {@code false} if the expression can yield a + * @return {@literal true} if the expression is a literal expression; {@literal false} if the expression can yield a * different result upon {@link #evaluate(ValueEvaluationContext) evaluation}. */ boolean isLiteral(); diff --git a/src/main/java/org/springframework/data/mapping/Alias.java b/src/main/java/org/springframework/data/mapping/Alias.java index 7f6f4d1b10..fe5de0394b 100644 --- a/src/main/java/org/springframework/data/mapping/Alias.java +++ b/src/main/java/org/springframework/data/mapping/Alias.java @@ -24,7 +24,7 @@ /** * A container object which may or may not contain a type alias value. If a value is present, {@code isPresent()} will - * return {@code true} and {@link #getValue()} will return the value. + * return {@literal true} and {@link #getValue()} will return the value. *

* Additional methods that depend on the presence or absence of a contained value are provided, such as * {@link #hasValue(Object)} or {@link #isPresent()} diff --git a/src/main/java/org/springframework/data/mapping/Parameter.java b/src/main/java/org/springframework/data/mapping/Parameter.java index b3b1b6d59e..0675c878bd 100644 --- a/src/main/java/org/springframework/data/mapping/Parameter.java +++ b/src/main/java/org/springframework/data/mapping/Parameter.java @@ -19,9 +19,9 @@ import java.util.Objects; import org.jspecify.annotations.Nullable; - import org.springframework.beans.factory.annotation.Value; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.data.annotation.Transient; import org.springframework.data.core.TypeInformation; import org.springframework.data.util.Lazy; import org.springframework.util.Assert; @@ -46,6 +46,7 @@ public class Parameter> { private final Lazy enclosingClassCache; private final Lazy hasExpression; + private final Lazy isTransient; /** * Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of @@ -80,6 +81,8 @@ public Parameter(@Nullable String name, TypeInformation type, Annotation[] an }); this.hasExpression = Lazy.of(() -> StringUtils.hasText(getValueExpression())); + this.isTransient = Lazy.of(() -> getAnnotations().isPresent(Transient.class) + || (entity != null && name != null && entity.isTransient(name))); } private static @Nullable String getValue(MergedAnnotations annotations) { @@ -177,6 +180,16 @@ public String getValueExpression() { return expression; } + /** + * @return if {@literal true} if the parameter is considered transient which means it is either annotated directly + * with {@link Transient} or the targeted {@link org.springframework.data.mapping.model.Property} is + * transient. + * @since 4.1 + */ + public boolean isTransient() { + return isTransient.get(); + } + /** * Returns the required expression to be used when looking up a source data structure to populate the actual parameter * value or throws {@link IllegalStateException} if there's no expression. diff --git a/src/main/java/org/springframework/data/mapping/PersistentEntity.java b/src/main/java/org/springframework/data/mapping/PersistentEntity.java index ed7661e9ff..e58c10c78a 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 @@ default P getRequiredIdProperty() { } /** - * 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 @@ default P getRequiredIdProperty() { 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 @@ default P getRequiredVersionProperty() { /** * 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 @@ default P getRequiredPersistentProperty(String name) { */ Iterable

getPersistentProperties(Class 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 4.1 + * @see #isTransient(String) + */ + @Nullable + P getTransientProperty(String name); + + /** + * Returns whether the property is transient. + * + * @param property name of the property. + * @return {@literal true} if the property is transient. Applies only for existing properties. {@literal false} if the + * property does not exist or is not transient. + * @since 4.1 + */ + 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 @@ default P getRequiredPersistentProperty(String name) { 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 @@ default P getRequiredPersistentProperty(String name) { 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 @@ default P getRequiredPersistentProperty(String name) { 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 @@ default A getRequiredAnnotation(Class annotationType) * * @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 @@ default A getRequiredAnnotation(Class annotationType) 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 38febd4bf4..177ad9011f 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 @@ private void createAndRegisterProperty(Property input) { P property = createPersistentProperty(input, entity, simpleTypeHolder); - if (property.isTransient()) { - return; - } - if (!input.isFieldBacked() && !property.usePropertyAccess()) { return; } diff --git a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java index c37a9f0a87..1b8413e025 100644 --- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java @@ -262,10 +262,10 @@ private Optional doFindAnnotation(Class annotationT } /** - * Returns whether the property carries the an annotation of the given type. + * Returns whether the property carries the annotation of the given type. * * @param annotationType the annotation type to look up. - * @return + * @return {@literal true} if the annotation is present, {@literal false} otherwise. */ @Override public boolean isAnnotationPresent(Class annotationType) { diff --git a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java index 67656df6fa..0528f95847 100644 --- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -72,11 +72,14 @@ public class BasicPersistentEntity> 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 BasicPersistentEntity(TypeInformation information, @Nullable Comparato 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 void addPersistentProperty(P property) { 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 P getPersistentProperty(String name) { return propertyCache.get(name); } + @Override + public @Nullable 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 annotationType) { diff --git a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java index 72be8bb742..bf7d71fc01 100644 --- a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java +++ b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java @@ -35,6 +35,7 @@ * @author Oliver Drotbohm * @author Mark Paluch * @author Johannes Englmeier + * @author Christoph Strobl * @since 2.3 */ public class InstantiationAwarePropertyAccessor implements PersistentPropertyAccessor { @@ -110,9 +111,16 @@ public void setProperty(PersistentProperty property, @Nullable Object value) @SuppressWarnings("NullAway") public @Nullable Object getParameterValue(Parameter parameter) { - return property.getName().equals(parameter.getName()) // - ? value - : delegate.getProperty(owner.getRequiredPersistentProperty(parameter.getName())); + if (property.getName().equals(parameter.getName())) { + return value; + } + + String paramName = parameter.getName(); + if (paramName != null && parameter.isTransient()) { + return ParameterValueProvider.getDefaultValue(parameter.getRawType()); + } + + return delegate.getProperty(owner.getRequiredPersistentProperty(paramName)); } }); } diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java b/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java index f4aa3813ed..d55cb36975 100644 --- a/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java +++ b/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java @@ -349,7 +349,7 @@ public Class getParameterType() { } /** - * @return {@code true} if the value hierarchy applies boxing. + * @return {@literal true} if the value hierarchy applies boxing. */ public boolean appliesBoxing() { return applyBoxing; diff --git a/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java index 1a490669bf..60b0d45578 100644 --- a/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java @@ -16,14 +16,15 @@ package org.springframework.data.mapping.model; import org.jspecify.annotations.Nullable; - import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.util.ReflectionUtils; /** * Callback interface to lookup values for a given {@link Parameter}. * * @author Oliver Gierke + * @author Christoph Strobl */ public interface ParameterValueProvider

> { @@ -34,4 +35,13 @@ public interface ParameterValueProvider

> { * @return the property value. Can be {@literal null}. */ @Nullable T getParameterValue(Parameter parameter); + + /** + * @param parameterType raw parameter type + * @return {@literal null} or primitive default for given parameter type. + * @since 4.1 + */ + static @Nullable Object getDefaultValue(Class parameterType) { + return parameterType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(parameterType) : null; + } } 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 c0a3a824af..0c875e99f6 100644 --- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java @@ -29,6 +29,7 @@ * * @author Oliver Gierke * @author Johannes Englmeier + * @author Mark Paluch */ public class PersistentEntityParameterValueProvider

> implements ParameterValueProvider

{ @@ -50,12 +51,15 @@ public PersistentEntityParameterValueProvider(PersistentEntity entity, Pro 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.isTransient()) { + return (T) ParameterValueProvider.getDefaultValue(parameter.getRawType()); + } if (name == null) { throw new MappingException(String.format("Parameter %s does not have a name", parameter)); diff --git a/src/main/java/org/springframework/data/projection/EntityProjection.java b/src/main/java/org/springframework/data/projection/EntityProjection.java index 4e69673778..0209a5c157 100644 --- a/src/main/java/org/springframework/data/projection/EntityProjection.java +++ b/src/main/java/org/springframework/data/projection/EntityProjection.java @@ -159,14 +159,14 @@ public TypeInformation getActualDomainType() { } /** - * @return {@code true} if the {@link #getMappedType()} is a projection. + * @return {@literal true} if the {@link #getMappedType()} is a projection. */ public boolean isProjection() { return projection; } /** - * @return {@code true} if the {@link #getMappedType()} is a closed projection. + * @return {@literal true} if the {@link #getMappedType()} is a closed projection. */ public boolean isClosedProjection() { return isProjection() diff --git a/src/main/java/org/springframework/data/projection/EntityProjectionIntrospector.java b/src/main/java/org/springframework/data/projection/EntityProjectionIntrospector.java index ad8538c21f..76a0c712ff 100644 --- a/src/main/java/org/springframework/data/projection/EntityProjectionIntrospector.java +++ b/src/main/java/org/springframework/data/projection/EntityProjectionIntrospector.java @@ -223,13 +223,13 @@ public interface ProjectionPredicate { * * @param target the target type. * @param underlyingType the underlying type. - * @return {@code true} if the input argument matches the predicate, otherwise {@code false}. + * @return {@literal true} if the input argument matches the predicate, otherwise {@literal false}. */ boolean test(Class target, Class underlyingType); /** * Return a composed predicate that represents a short-circuiting logical AND of this predicate and another. When - * evaluating the composed predicate, if this predicate is {@code false}, then the {@code other} predicate is not + * evaluating the composed predicate, if this predicate is {@literal false}, then the {@code other} predicate is not * evaluated. *

* Any exceptions thrown during evaluation of either predicate are relayed to the caller; if evaluation of this diff --git a/src/main/java/org/springframework/data/repository/config/PropertiesBasedNamedQueriesFactoryBean.java b/src/main/java/org/springframework/data/repository/config/PropertiesBasedNamedQueriesFactoryBean.java index efee797d97..daa0ee7817 100644 --- a/src/main/java/org/springframework/data/repository/config/PropertiesBasedNamedQueriesFactoryBean.java +++ b/src/main/java/org/springframework/data/repository/config/PropertiesBasedNamedQueriesFactoryBean.java @@ -48,7 +48,7 @@ public class PropertiesBasedNamedQueriesFactoryBean extends PropertiesLoaderSupp * Set whether a shared singleton {@code PropertiesBasedNamedQueries} instance should be created, or rather a new * {@code PropertiesBasedNamedQueries} instance on each request. *

- * Default is {@code true} (a shared singleton). + * Default is {@literal true} (a shared singleton). */ public void setSingleton(boolean singleton) { this.singleton = singleton; diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java index c5d84f8986..49af8690c5 100644 --- a/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java +++ b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java @@ -35,7 +35,7 @@ public class RepositoryMethodContextHolder { /** * ThreadLocal holder for repository method associated with this thread. Will contain {@code null} unless the - * "exposeMetadata" property on the controlling repository factory configuration has been set to {@code true}. + * "exposeMetadata" property on the controlling repository factory configuration has been set to {@literal true}. */ private static final ThreadLocal currentMethod = new NamedThreadLocal<>( "Current Repository Method"); diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodLookup.java b/src/main/java/org/springframework/data/repository/core/support/MethodLookup.java index d475a65fec..5f21a30724 100644 --- a/src/main/java/org/springframework/data/repository/core/support/MethodLookup.java +++ b/src/main/java/org/springframework/data/repository/core/support/MethodLookup.java @@ -52,8 +52,8 @@ public interface MethodLookup { /** * Returns a composed {@link MethodLookup} that represents a concatenation of this predicate and another. When - * evaluating the composed method lookup, if this lookup evaluates {@code true}, then the {@code other} method lookup - * is not evaluated. + * evaluating the composed method lookup, if this lookup evaluates {@literal true}, then the {@code other} method + * lookup is not evaluated. * * @param other must not be {@literal null}. * @return the composed {@link MethodLookup}. diff --git a/src/main/java/org/springframework/data/spel/spi/Function.java b/src/main/java/org/springframework/data/spel/spi/Function.java index 42f425b55b..e92f2a8cb7 100644 --- a/src/main/java/org/springframework/data/spel/spi/Function.java +++ b/src/main/java/org/springframework/data/spel/spi/Function.java @@ -154,7 +154,7 @@ public int getParameterCount() { * Checks if the encapsulated method has exactly the argument types as those passed as an argument. * * @param argumentTypes a list of {@link TypeDescriptor}s to compare with the argument types of the method - * @return {@code true} if the types are equal, {@code false} otherwise. + * @return {@literal true} if the types are equal, {@literal false} otherwise. */ public boolean supportsExact(List argumentTypes) { return ParameterTypes.of(argumentTypes).exactlyMatchParametersOf(method); @@ -164,7 +164,7 @@ public boolean supportsExact(List argumentTypes) { * Checks whether this {@code Function} has the same signature as another {@code Function}. * * @param other the {@code Function} to compare {@code this} with. - * @return {@code true} if name and argument list are the same. + * @return {@literal true} if name and argument list are the same. */ public boolean isSignatureEqual(Function other) { diff --git a/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java b/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java index f865414db1..ef0941d285 100644 --- a/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java @@ -136,7 +136,7 @@ public static Class getReturnType(Method method) { * Returns whether the given {@link KType} is a {@link KClass#isValue() value} class. * * @param type the kotlin type to inspect. - * @return {@code true} the type is a value class. + * @return {@literal true} the type is a value class. * @since 3.2 */ public static boolean isValueClass(KType type) { @@ -148,7 +148,7 @@ public static boolean isValueClass(KType type) { * Returns whether the given class makes uses Kotlin {@link KClass#isValue() value} classes. * * @param type the kotlin type to inspect. - * @return {@code true} when at least one property uses Kotlin value classes. + * @return {@literal true} when at least one property uses Kotlin value classes. * @since 3.2 */ public static boolean hasValueClassProperty(Class type) { diff --git a/src/main/java/org/springframework/data/util/Predicates.java b/src/main/java/org/springframework/data/util/Predicates.java index e9dcff34cd..885eda0868 100644 --- a/src/main/java/org/springframework/data/util/Predicates.java +++ b/src/main/java/org/springframework/data/util/Predicates.java @@ -60,18 +60,18 @@ static Predicate declaringClass(Predicate> predic } /** - * A {@link Predicate} that yields always {@code true}. + * A {@link Predicate} that yields always {@literal true}. * - * @return a {@link Predicate} that yields always {@code true}. + * @return a {@link Predicate} that yields always {@literal true}. */ static Predicate isTrue() { return t -> true; } /** - * A {@link Predicate} that yields always {@code false}. + * A {@link Predicate} that yields always {@literal false}. * - * @return a {@link Predicate} that yields always {@code false}. + * @return a {@link Predicate} that yields always {@literal false}. */ static Predicate isFalse() { return t -> false; diff --git a/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java b/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java index a38b907a8a..459a6999d4 100644 --- a/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java @@ -43,7 +43,7 @@ public interface OffsetScrollPositionArgumentResolver extends HandlerMethodArgum * wrapped arguments in {@link java.util.Optional}. * * @param parameter the method parameter to resolve. This parameter must have previously been passed to - * {@link #supportsParameter} which must have returned {@code true}. + * {@link #supportsParameter} which must have returned {@literal true}. * @param mavContainer the ModelAndViewContainer for the current request * @param webRequest the current request * @param binderFactory a factory for creating {@link WebDataBinder} instances diff --git a/src/main/java/org/springframework/data/web/PageableArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java index b99dd2a783..c8885dae14 100644 --- a/src/main/java/org/springframework/data/web/PageableArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java @@ -41,7 +41,7 @@ public interface PageableArgumentResolver extends HandlerMethodArgumentResolver * Resolves a {@link Pageable} method parameter into an argument value from a given request. * * @param parameter the method parameter to resolve. This parameter must have previously been passed to - * {@link #supportsParameter} which must have returned {@code true}. + * {@link #supportsParameter} which must have returned {@literal true}. * @param mavContainer the ModelAndViewContainer for the current request * @param webRequest the current request * @param binderFactory a factory for creating {@link WebDataBinder} instances diff --git a/src/main/java/org/springframework/data/web/SortArgumentResolver.java b/src/main/java/org/springframework/data/web/SortArgumentResolver.java index ee9e1f8873..60f0554e02 100644 --- a/src/main/java/org/springframework/data/web/SortArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/SortArgumentResolver.java @@ -41,7 +41,7 @@ public interface SortArgumentResolver extends HandlerMethodArgumentResolver { * Resolves a {@link Sort} method parameter into an argument value from a given request. * * @param parameter the method parameter to resolve. This parameter must have previously been passed to - * {@link #supportsParameter} which must have returned {@code true}. + * {@link #supportsParameter} which must have returned {@literal true}. * @param mavContainer the ModelAndViewContainer for the current request * @param webRequest the current request * @param binderFactory a factory for creating {@link WebDataBinder} instances diff --git a/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java b/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java index c1b9addcfa..940f46429a 100755 --- a/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java +++ b/src/test/java/org/springframework/data/auditing/AuditingHandlerUnitTests.java @@ -96,7 +96,7 @@ void setsAuditorIfConfigured() { } /** - * Checks that the advice does not set modification information on creation if the falg is set to {@code false}. + * Checks that the advice does not set modification information on creation if the falg is set to {@literal false}. */ @Test void honoursModifiedOnCreationFlag() { diff --git a/src/test/java/org/springframework/data/convert/DtoInstantiatingConverterUnitTests.java b/src/test/java/org/springframework/data/convert/DtoInstantiatingConverterUnitTests.java index a511b3beb3..5fce08844c 100644 --- a/src/test/java/org/springframework/data/convert/DtoInstantiatingConverterUnitTests.java +++ b/src/test/java/org/springframework/data/convert/DtoInstantiatingConverterUnitTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Reference; +import org.springframework.data.annotation.Transient; import org.springframework.data.mapping.context.SampleMappingContext; import org.springframework.data.mapping.model.EntityInstantiators; @@ -27,10 +28,11 @@ * Unit tests for {@link DtoInstantiatingConverter}. * * @author Mark Paluch + * @author Christoph Strobl */ class DtoInstantiatingConverterUnitTests { - @Test // GH- 3104 + @Test // GH-3104 void dtoProjectionShouldConsiderPropertiesAndAssociations() { TheOtherThing ref = new TheOtherThing(); @@ -44,6 +46,20 @@ void dtoProjectionShouldConsiderPropertiesAndAssociations() { assertThat(projection.ref).isSameAs(ref); } + @Test // GH-2942 + void shouldDefaultTransientConstructorParameterWhenConvertingToDto() { + + SourceEntity source = new SourceEntity("1", "Alice"); + DtoInstantiatingConverter converter = new DtoInstantiatingConverter(DtoWithTransientParam.class, + new SampleMappingContext(), new EntityInstantiators()); + + DtoWithTransientParam dto = (DtoWithTransientParam) converter.convert(source); + + assertThat(dto.id).isEqualTo("1"); + assertThat(dto.name).isEqualTo("Alice"); + assertThat(dto.summary).isNull(); + } + static class MyAssociativeEntity { String id; @@ -66,6 +82,19 @@ static class DtoProjection { @Reference TheOtherThing ref; } + static class SourceEntity { + + String id; + String name; + + public SourceEntity(String id, String name) { + this.id = id; + this.name = name; + } + } + + record DtoWithTransientParam(String id, String name, @Transient String summary) {} + static class TheOtherThing { String id; } diff --git a/src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java b/src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java index 9fe20fa06a..c6d9d39353 100644 --- a/src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java @@ -18,6 +18,7 @@ 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; import org.springframework.data.mapping.model.EntityInstantiators; @@ -28,6 +29,7 @@ * * @author Oliver Drotbohm * @author Mark Paluch + * @author Christoph Strobl */ class InstantiationAwarePersistentPropertyAccessorUnitTests { @@ -88,6 +90,32 @@ void shouldSetPropertyOfRecordUsingCanonicalConstructor() { assertThat(wrapper.getBean()).isEqualTo(new WithSingleArgConstructor(41L, "Oliver August")); } + /** + * Reproduces failure: when an entity has both persistent and transient constructor parameters, setting a + * persistent property via the copy path (InstantiationAwarePropertyAccessor) should succeed and leave the + * transient parameter at its default (null). Currently throws IllegalStateException because + * getRequiredPersistentProperty(transientParamName) is used for constructor arguments. + */ + @Test // GH-2942 + void shouldSetPersistentPropertyWhenEntityHasTransientConstructorParameter() { + + var instantiators = new EntityInstantiators(); + var context = new SampleMappingContext(); + + PersistentEntity entity = context + .getRequiredPersistentEntity(RecordWithPersistentAndTransientParams.class); + + var bean = new RecordWithPersistentAndTransientParams(42L, "Alice", null); + + PersistentPropertyAccessor wrapper = new InstantiationAwarePropertyAccessor<>( + bean, entity::getPropertyAccessor, instantiators); + + wrapper.setProperty(entity.getRequiredPersistentProperty("name"), "Bob"); + wrapper.setProperty(entity.getRequiredPersistentProperty("id"), 42L); + + assertThat(wrapper.getBean()).isEqualTo(new RecordWithPersistentAndTransientParams(42L, "Bob", null)); + } + record Sample(String firstname, String lastname, int age) { } @@ -99,4 +127,5 @@ public WithSingleArgConstructor(String name) { } } + record RecordWithPersistentAndTransientParams(Long id, String name, @Transient String displayName) {} } 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 c3c3e03d01..52ee422855 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 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 @@ void returnsTypeAliasIfAnnotated() { @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 @@ void returnsGeneratedPropertyAccessorForPropertyAccessor() { assertThat(accessor).isNotInstanceOf(BeanWrapper.class); assertThat(accessor).isInstanceOfSatisfying(InstantiationAwarePropertyAccessor.class, it -> { - var delegateFunction = (Function>) ReflectionTestUtils - .getField(it, "delegateFunction"); + var delegateFunction = (Function>) ReflectionTestUtils.getField(it, + "delegateFunction"); var delegate = delegateFunction.apply(value); assertThat(delegate.getClass().getName()).contains("_Accessor_"); @@ -360,6 +361,18 @@ void exposesPropertyPopulationNotRequired() { .forEach(it -> assertThat(createPopulatedPersistentEntity(it).requiresPropertyPopulation()).isFalse()); } + @ParameterizedTest // GH-1432 + @ValueSource(classes = { WithTransient.class, RecordWithTransient.class, DataClassWithTransientProperty.class }) + void includesTransientProperty(Class classUnderTest) { + + PersistentEntity entity = createPopulatedPersistentEntity(classUnderTest); + + assertThat(entity).extracting(PersistentProperty::getName).hasSize(1).containsOnly("firstname"); + assertThat(entity.isTransient("firstname")).isFalse(); + assertThat(entity.isTransient("lastname")).isTrue(); + assertThat(entity.getTransientProperty("lastname").getName()).isEqualTo("lastname"); + } + @Test // #2325 void doWithAllInvokesPropertyHandlerForBothAPropertiesAndAssociations() { @@ -476,6 +489,17 @@ public PropertyPopulationNotRequiredWithTransient(String firstname, String lastn } } + private static class WithTransient { + + String firstname; + @Transient String lastname; + + } + + record RecordWithTransient(String firstname, @Transient String lastname) { + + } + // #2325 static class WithAssociation { @@ -483,4 +507,5 @@ static class WithAssociation { String property; @Reference WithAssociation association; } + } diff --git a/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorIntegrationTests.java b/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorIntegrationTests.java new file mode 100644 index 0000000000..57cc6ffce0 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorIntegrationTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023-present 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 createInstance(Class entityType) { + + var entity = context.getRequiredPersistentEntity(entityType); + var instantiator = instantiators.getInstantiatorFor(entity); + + return (E) instantiator.createInstance(entity, + new PersistentEntityParameterValueProvider<>(entity, new PropertyValueProvider() { + @Override + public 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) { + + } + +} diff --git a/src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt b/src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt index 4479c6c0a1..7cc04451d3 100644 --- a/src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt +++ b/src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt @@ -18,6 +18,7 @@ package org.springframework.data.mapping.model import org.jmolecules.ddd.types.AggregateRoot import org.jmolecules.ddd.types.Identifier import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Transient import java.time.LocalDateTime /** @@ -55,6 +56,10 @@ data class SingleSettableProperty constructor(val id: Double = Math.random()) { val version: Int? = null } +// note: Kotlin ships also a @Transient annotation to indicate JVM's transient keyword. +data class DataClassWithTransientProperty(val firstname: String, @Transient val lastname: String) +data class DataClassWithTransientProperties(@Transient val foo: String = "foo", @Transient val bar: Int) + data class WithCustomCopyMethod( val id: String?, val userId: String,