From 34f94dccd65b5ba56a02292934da7ca1e9c19283 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 21 Nov 2023 09:42:36 +0100 Subject: [PATCH] Include transient properties in persistent entity metamodel. --- .../data/mapping/PersistentEntity.java | 54 +++++++++++++------ .../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, 110 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/springframework/data/mapping/PersistentEntity.java b/src/main/java/org/springframework/data/mapping/PersistentEntity.java index ed7661e9f..81dac1fc8 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/PersistentEntity.java @@ -49,8 +49,8 @@ public interface PersistentEntity> extends It * * @return {@literal null} in case no suitable creation mechanism for automatic construction can be found. This * usually indicates that the instantiation of the object of that persistent entity is done through either a - * customer {@link org.springframework.data.mapping.model.EntityInstantiator} or handled by custom - * conversion mechanisms entirely. + * customer {@link org.springframework.data.mapping.model.EntityInstantiator} or handled by custom conversion + * mechanisms entirely. * @since 3.0 */ @Nullable @@ -110,8 +110,8 @@ public interface PersistentEntity> extends It } /** - * Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property - * is available on the entity. + * Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property is + * available on the entity. * * @return the version property of the {@link PersistentEntity}. */ @@ -119,8 +119,8 @@ public interface PersistentEntity> extends It P getVersionProperty(); /** - * Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property - * is available on the entity. + * Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property is + * available on the entity. * * @return the version property of the {@link PersistentEntity}. * @throws IllegalStateException if {@link PersistentEntity} does not define a {@literal version} property. @@ -140,7 +140,7 @@ public interface PersistentEntity> extends It /** * Obtains a {@link PersistentProperty} instance by name. * - * @param name The name of the property. Can be {@literal null}. + * @param name the name of the property. Can be {@literal null}. * @return the {@link PersistentProperty} or {@literal null} if it doesn't exist. */ @Nullable @@ -186,6 +186,28 @@ public interface PersistentEntity> extends It */ Iterable

getPersistentProperties(Class annotationType); + /** + * Obtains a transient {@link PersistentProperty} instance by name. You can check with {@link #isTransient(String)} + * whether there is a transient property before calling this method. + * + * @param name the name of the property. Can be {@literal null}. + * @return the {@link PersistentProperty} or {@literal null} if it doesn't exist. + * @since 3.3 + * @see #isTransient(String) + */ + @Nullable + P getTransientProperty(String name); + + /** + * Returns whether the property is transient. + * + * @param property name of the property. + * @return {@code true} if the property is transient. Applies only for existing properties. {@code false} if the + * property does not exist or is not transient. + * @since 3.3 + */ + boolean isTransient(String property); + /** * Returns whether the {@link PersistentEntity} has an id property. If this call returns {@literal true}, * {@link #getIdProperty()} will return a non-{@literal null} value. @@ -210,8 +232,8 @@ public interface PersistentEntity> extends It Class getType(); /** - * Returns the alias to be used when storing type information. Might be {@literal null} to indicate that there was - * no alias defined through the mapping metadata. + * Returns the alias to be used when storing type information. Might be {@literal null} to indicate that there was no + * alias defined through the mapping metadata. * * @return */ @@ -241,8 +263,8 @@ public interface PersistentEntity> extends It void doWithProperties(SimplePropertyHandler handler); /** - * Applies the given {@link AssociationHandler} to all {@link Association} contained in this - * {@link PersistentEntity}. The iteration order is undefined. + * Applies the given {@link AssociationHandler} to all {@link Association} contained in this {@link PersistentEntity}. + * The iteration order is undefined. * * @param handler must not be {@literal null}. */ @@ -257,8 +279,8 @@ public interface PersistentEntity> extends It void doWithAssociations(SimpleAssociationHandler handler); /** - * Applies the given {@link PropertyHandler} to both all {@link PersistentProperty}s as well as all inverse - * properties of all {@link Association}s. The iteration order is undefined. + * Applies the given {@link PropertyHandler} to both all {@link PersistentProperty}s as well as all inverse properties + * of all {@link Association}s. The iteration order is undefined. * * @param handler must not be {@literal null}. * @since 2.5 @@ -342,7 +364,7 @@ public interface PersistentEntity> extends It * * @param bean must not be {@literal null}. * @throws IllegalArgumentException in case the given bean is not an instance of the typ represented by the - * {@link PersistentEntity}. + * {@link PersistentEntity}. * @return whether the given bean is considered a new instance. */ boolean isNew(Object bean); @@ -358,8 +380,8 @@ public interface PersistentEntity> extends It boolean isImmutable(); /** - * Returns whether the entity needs properties to be populated, i.e. if any property exists that's not initialized - * by the constructor. + * Returns whether the entity needs properties to be populated, i.e. if any property exists that's not initialized by + * the constructor. * * @return * @since 2.1 diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index 38febd4bf..177ad9011 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -646,10 +646,6 @@ public abstract class AbstractMappingContext> private final @Nullable InstanceCreatorMetadata

creator; private final TypeInformation information; private final List

properties; + private final List

transientProperties; private final List

persistentPropertiesCache; private final @Nullable Comparator

comparator; private final Set> associations; private final Map propertyCache; + + private final Map transientPropertyCache; private final Map, Optional> annotationCache; private final MultiValueMap, P> propertyAnnotationCache; @@ -114,12 +117,14 @@ public class BasicPersistentEntity> this.information = information; this.properties = new ArrayList<>(); + this.transientProperties = new ArrayList<>(0); this.persistentPropertiesCache = new ArrayList<>(); this.comparator = comparator; this.creator = InstanceCreatorMetadataDiscoverer.discover(this); this.associations = comparator == null ? new HashSet<>() : new TreeSet<>(new AssociationComparator<>(comparator)); this.propertyCache = new HashMap<>(16, 1.0f); + this.transientPropertyCache = new HashMap<>(0, 1f); this.annotationCache = new ConcurrentHashMap<>(16); this.propertyAnnotationCache = CollectionUtils.toMultiValueMap(new ConcurrentHashMap<>(16)); this.propertyAccessorFactory = BeanWrapperPropertyAccessorFactory.INSTANCE; @@ -186,6 +191,18 @@ public class BasicPersistentEntity> Assert.notNull(property, "Property must not be null"); + if (property.isTransient()) { + + if (transientProperties.contains(property)) { + return; + } + + transientProperties.add(property); + transientPropertyCache.put(property.getName(), property); + + return; + } + if (properties.contains(property)) { return; } @@ -279,6 +296,19 @@ public class BasicPersistentEntity> return propertyCache.get(name); } + @Override + public P getTransientProperty(String name) { + return transientPropertyCache.get(name); + } + + @Override + public boolean isTransient(String property) { + + P transientProperty = getTransientProperty(property); + + return transientProperty != null && transientProperty.isTransient(); + } + @Override public Iterable

getPersistentProperties(Class annotationType) { diff --git a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java index c0a3a824a..cfac7c070 100644 --- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java @@ -17,6 +17,7 @@ package org.springframework.data.mapping.model; import org.jspecify.annotations.Nullable; +import org.springframework.data.annotation.Transient; import org.springframework.data.mapping.InstanceCreatorMetadata; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.Parameter; @@ -55,6 +56,13 @@ public class PersistentEntityParameterValueProvider

> { @SuppressWarnings("unchecked") void considersComparatorForPropertyOrder() { - var entity = createEntity(Person.class, - Comparator.comparing(PersistentProperty::getName)); + var entity = createEntity(Person.class, Comparator.comparing(PersistentProperty::getName)); var lastName = (T) Mockito.mock(PersistentProperty.class); when(lastName.getName()).thenReturn("lastName"); @@ -199,8 +200,8 @@ class BasicPersistentEntityUnitTests> { assertThat(accessor).isNotInstanceOf(BeanWrapper.class); assertThat(accessor).isInstanceOfSatisfying(InstantiationAwarePropertyAccessor.class, it -> { - var delegateFunction = (Function>) 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 @@ class BasicPersistentEntityUnitTests> { .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 @@ class BasicPersistentEntityUnitTests> { } } + private static class WithTransient { + + String firstname; + @Transient String lastname; + + } + + record RecordWithTransient(String firstname, @Transient String lastname) { + + } + // #2325 static class WithAssociation { @@ -483,4 +507,5 @@ class BasicPersistentEntityUnitTests> { 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 4479c6c0a..7cc04451d 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,