From f2ecc51ca3f8bdd4c0e2127f0eb2f5991585a32b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 21 Nov 2023 09:42:24 +0100 Subject: [PATCH 1/4] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d55079dbe8..764e43dd59 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.4.0-SNAPSHOT + 3.4.0-ISSUE-1432-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From 26f6ff373cd802f0f9349c0b8bdbe1cf8db55422 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 21 Nov 2023 09:42:36 +0100 Subject: [PATCH 2/4] Include transient properties in persistent entity metamodel. --- .../data/mapping/PersistentEntity.java | 60 +++++++++++++------ .../context/AbstractMappingContext.java | 4 -- .../mapping/model/BasicPersistentEntity.java | 30 ++++++++++ ...ersistentEntityParameterValueProvider.java | 8 +++ .../model/BasicPersistentEntityUnitTests.java | 33 ++++++++-- .../data/mapping/model/DataClasses.kt | 5 ++ 6 files changed, 113 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/springframework/data/mapping/PersistentEntity.java b/src/main/java/org/springframework/data/mapping/PersistentEntity.java index b460f72f99..1b910eb99d 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/PersistentEntity.java @@ -47,9 +47,9 @@ public interface PersistentEntity> extends It * Returns the {@link PreferredConstructor} to be used to instantiate objects of this {@link PersistentEntity}. * * @return {@literal null} in case no suitable constructor 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. + * 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. * @deprecated since 3.0, use {@link #getInstanceCreatorMetadata()}. */ @Nullable @@ -61,8 +61,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 @@ -136,8 +136,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}. */ @@ -145,8 +145,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. @@ -166,7 +166,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 @@ -213,6 +213,28 @@ default P getPersistentProperty(Class annotationType) { */ 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 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. @@ -237,8 +259,8 @@ default P getPersistentProperty(Class annotationType) { 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 */ @@ -268,8 +290,8 @@ default P getPersistentProperty(Class annotationType) { 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}. */ @@ -284,8 +306,8 @@ default P getPersistentProperty(Class annotationType) { 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 @@ -370,7 +392,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); @@ -386,8 +408,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 ae632b3b8e..104b2455ab 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -643,10 +643,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/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java index 249177bd86..fc090a7d4a 100644 --- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -68,11 +68,14 @@ public class BasicPersistentEntity> implement 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; @@ -110,12 +113,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)); @@ -190,6 +195,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; } @@ -274,6 +291,19 @@ public P getPersistentProperty(String name) { 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 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 708c91dd6d..fafdbf8e9d 100644 --- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mapping.model; +import org.springframework.data.annotation.Transient; import org.springframework.data.mapping.InstanceCreatorMetadata; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.Parameter; @@ -56,6 +57,13 @@ public T getParameterValue(Parameter parameter) { return (T) parent; } + if (parameter.getAnnotations().isPresent(Transient.class)) { + + // parameter.getRawType().isPrimitive() + return null; + + } + String name = parameter.getName(); if (name == null) { 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 920aa5542c..7b35db707f 100755 --- a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java @@ -31,6 +31,8 @@ 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; @@ -105,8 +107,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"); @@ -198,8 +199,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_"); @@ -359,6 +360,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() { @@ -475,6 +488,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 { @@ -482,4 +506,5 @@ static class WithAssociation { String property; @Reference WithAssociation association; } + } 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 e5627e1242..f591a9fae9 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, From e15ea58993f269f3176b5f65367d075384b50ec0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 21 Nov 2023 11:25:42 +0100 Subject: [PATCH 3/4] 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. --- .../modules/ROOT/pages/object-mapping.adoc | 12 ++- .../data/annotation/PersistenceCreator.java | 5 +- .../data/annotation/Transient.java | 13 ++- ...ersistentEntityParameterValueProvider.java | 21 +++-- .../EntityInstantiatorIntegrationTests.java | 94 +++++++++++++++++++ 5 files changed, 128 insertions(+), 17 deletions(-) create mode 100644 src/test/java/org/springframework/data/mapping/model/EntityInstantiatorIntegrationTests.java diff --git a/src/main/antora/modules/ROOT/pages/object-mapping.adoc b/src/main/antora/modules/ROOT/pages/object-mapping.adoc index 380966c536..f67921a872 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 53616aec42..b4d0ad8ef8 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 @@ -29,4 +31,5 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) -public @interface PersistenceCreator {} +public @interface PersistenceCreator { +} diff --git a/src/main/java/org/springframework/data/annotation/Transient.java b/src/main/java/org/springframework/data/annotation/Transient.java index 90b2a398d5..f3591fa9cf 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/mapping/model/PersistentEntityParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java index fafdbf8e9d..0770e8c1ba 100644 --- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java @@ -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

> implements ParameterValueProvider

{ @@ -47,25 +49,26 @@ public PersistentEntityParameterValueProvider(PersistentEntity entity, Pro } @Override + @Nullable + private static Object getTransientDefault(Class 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; } - 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)); } 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..1e21975342 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorIntegrationTests.java @@ -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 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) { + + } + +} From df6ca3de37ed2573b4d6b7f187eefebfddac43db Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 4 Nov 2024 14:30:37 +0100 Subject: [PATCH 4/4] Polishing. Add missing Override annotations, consistently use literal tags for boolean values. --- .../data/auditing/AuditingHandlerSupport.java | 4 ++-- .../org/springframework/data/domain/Window.java | 6 +++--- .../data/expression/ValueExpression.java | 2 +- .../org/springframework/data/mapping/Alias.java | 2 +- .../data/mapping/PersistentEntity.java | 2 +- .../AnnotationBasedPersistentProperty.java | 4 ++-- .../data/mapping/model/KotlinValueUtils.java | 2 +- .../PersistentEntityParameterValueProvider.java | 17 ++++++++--------- .../data/projection/EntityProjection.java | 4 ++-- .../EntityProjectionIntrospector.java | 4 ++-- .../PropertiesBasedNamedQueriesFactoryBean.java | 2 +- .../core/RepositoryMethodContextHolder.java | 2 +- .../repository/core/support/MethodLookup.java | 4 ++-- .../springframework/data/spel/spi/Function.java | 4 ++-- .../data/util/KotlinReflectionUtils.java | 4 ++-- .../springframework/data/util/Predicates.java | 8 ++++---- .../OffsetScrollPositionArgumentResolver.java | 2 +- .../data/web/PageableArgumentResolver.java | 2 +- .../data/web/SortArgumentResolver.java | 2 +- .../data/auditing/AuditingHandlerUnitTests.java | 2 +- 20 files changed, 39 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java b/src/main/java/org/springframework/data/auditing/AuditingHandlerSupport.java index 489a2e6457..750f3e9b44 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/domain/Window.java b/src/main/java/org/springframework/data/domain/Window.java index d5c220b27c..9303bed20a 100644 --- a/src/main/java/org/springframework/data/domain/Window.java +++ b/src/main/java/org/springframework/data/domain/Window.java @@ -68,9 +68,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(); @@ -102,7 +102,7 @@ default boolean isLast() { * Returns whether the underlying scroll mechanism can provide a {@link ScrollPosition} at {@code index}. * * @param index - * @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 7ad6ad4d26..b5e701d468 100644 --- a/src/main/java/org/springframework/data/expression/ValueExpression.java +++ b/src/main/java/org/springframework/data/expression/ValueExpression.java @@ -48,7 +48,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 7d581285b6..fb488af038 100644 --- a/src/main/java/org/springframework/data/mapping/Alias.java +++ b/src/main/java/org/springframework/data/mapping/Alias.java @@ -22,7 +22,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/PersistentEntity.java b/src/main/java/org/springframework/data/mapping/PersistentEntity.java index 1b910eb99d..a3536533d8 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/PersistentEntity.java @@ -229,7 +229,7 @@ default P getPersistentProperty(Class annotationType) { * 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 + * @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 3.3 */ 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 4bd60cc391..6e122de724 100644 --- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java @@ -264,10 +264,10 @@ public A findPropertyOrOwnerAnnotation(Class annotatio } /** - * 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/KotlinValueUtils.java b/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java index f2fba6358e..153060c2ca 100644 --- a/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java +++ b/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java @@ -348,7 +348,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/PersistentEntityParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java index 0770e8c1ba..2093f8e4ac 100644 --- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java @@ -26,9 +26,7 @@ /** * {@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. + * the value of the property referenced by the given {@link Parameter}. * * @author Oliver Gierke * @author Johannes Englmeier @@ -42,18 +40,13 @@ public class PersistentEntityParameterValueProvider

entity, PropertyValueProvider

provider, - Object parent) { + @Nullable Object parent) { this.entity = entity; this.provider = provider; this.parent = parent; } @Override - @Nullable - private static Object getTransientDefault(Class parameterType) { - return parameterType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(parameterType) : null; - } - @Nullable @SuppressWarnings("unchecked") public T getParameterValue(Parameter parameter) { @@ -82,4 +75,10 @@ public T getParameterValue(Parameter parameter) { return provider.getPropertyValue(property); } + + @Nullable + private static Object getTransientDefault(Class parameterType) { + return parameterType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(parameterType) : null; + } + } diff --git a/src/main/java/org/springframework/data/projection/EntityProjection.java b/src/main/java/org/springframework/data/projection/EntityProjection.java index f3941182e2..60fd4058a0 100644 --- a/src/main/java/org/springframework/data/projection/EntityProjection.java +++ b/src/main/java/org/springframework/data/projection/EntityProjection.java @@ -158,14 +158,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 88335a22dd..a0396bc020 100644 --- a/src/main/java/org/springframework/data/projection/EntityProjectionIntrospector.java +++ b/src/main/java/org/springframework/data/projection/EntityProjectionIntrospector.java @@ -222,13 +222,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 02f3f16901..7e7b7e28d5 100644 --- a/src/main/java/org/springframework/data/repository/config/PropertiesBasedNamedQueriesFactoryBean.java +++ b/src/main/java/org/springframework/data/repository/config/PropertiesBasedNamedQueriesFactoryBean.java @@ -47,7 +47,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 bc57f152f7..b52ee50e38 100644 --- a/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java +++ b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java @@ -34,7 +34,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 6b1213e9c9..c0a5fe0c94 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 @@ -49,8 +49,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 d315c84b18..0d5a02fd61 100644 --- a/src/main/java/org/springframework/data/spel/spi/Function.java +++ b/src/main/java/org/springframework/data/spel/spi/Function.java @@ -152,7 +152,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); @@ -162,7 +162,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 f958bdc520..f6ad333f5d 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 ff50758c36..e9afbe295c 100644 --- a/src/main/java/org/springframework/data/util/Predicates.java +++ b/src/main/java/org/springframework/data/util/Predicates.java @@ -52,18 +52,18 @@ public interface Predicates { Predicate IS_BRIDGE_METHOD = Method::isBridge; /** - * 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 6812251416..e6dc1e3a29 100644 --- a/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/OffsetScrollPositionArgumentResolver.java @@ -42,7 +42,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 32e1e15793..97aa5c93e6 100644 --- a/src/main/java/org/springframework/data/web/PageableArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java @@ -40,7 +40,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 bba155f8af..cff7aeffad 100644 --- a/src/main/java/org/springframework/data/web/SortArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/SortArgumentResolver.java @@ -40,7 +40,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 46c1c9abf5..c8786f2809 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() {