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,