Filter delegated properties for Kotlin data classes.

We now filter delegated properties (such as lazy) from being managed as persistent properties.

Closes #3112
This commit is contained in:
Mark Paluch
2024-06-24 11:41:46 +02:00
parent 292ab2399e
commit dc82813d8c
4 changed files with 81 additions and 3 deletions

View File

@@ -314,7 +314,7 @@ Consider the following `data` class `Person`:
data class Person(val id: String, val name: String)
----
The class above compiles to a typical class with an explicit constructor.We can customize this class by adding another constructor and annotate it with `@PersistenceCreator` to indicate a constructor preference:
The class above compiles to a typical class with an explicit constructor. We can customize this class by adding another constructor and annotate it with `@PersistenceCreator` to indicate a constructor preference:
[source,kotlin]
----
@@ -335,6 +335,9 @@ data class Person(var id: String, val name: String = "unknown")
Every time the `name` parameter is either not part of the result or its value is `null`, then the `name` defaults to `unknown`.
NOTE: Delegated properties are not supported with Spring Data. The mapping metadata filters delegated properties for Kotlin Data classes.
In all other cases you can exclude synthetic fields for delegated properties by annotating the property with `@delegate:org.springframework.data.annotation.Transient`.
[[property-population-of-kotlin-data-classes]]
=== Property population of Kotlin data classes

View File

@@ -726,6 +726,7 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
matches.add(new PropertyMatch("class", null));
matches.add(new PropertyMatch("this\\$.*", null));
matches.add(new PropertyMatch("metaClass", "groovy.lang.MetaClass"));
matches.add(new KotlinDataClassPropertyMatch(".*\\$delegate", null));
UNMAPPED_PROPERTIES = Streamable.of(matches);
}
@@ -738,7 +739,7 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
}
return UNMAPPED_PROPERTIES.stream()//
.noneMatch(it -> it.matches(field.getName(), field.getType()));
.noneMatch(it -> it.matches(field));
}
/**
@@ -756,7 +757,7 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
}
return UNMAPPED_PROPERTIES.stream()//
.noneMatch(it -> it.matches(property.getName(), property.getType()));
.noneMatch(it -> it.matches(property));
}
/**
@@ -788,6 +789,26 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
/**
* Returns whether the given {@link Field} matches the defined {@link PropertyMatch}.
*
* @param field must not be {@literal null}.
* @return
*/
public boolean matches(Field field) {
return matches(field.getName(), field.getType());
}
/**
* Returns whether the given {@link Property} matches the defined {@link PropertyMatch}.
*
* @param property must not be {@literal null}.
* @return
*/
public boolean matches(Property property) {
return matches(property.getName(), property.getType());
}
/**
* Returns whether the given field name and type matches the defined {@link PropertyMatch}.
*
* @param name must not be {@literal null}.
* @param type must not be {@literal null}.
* @return
@@ -808,5 +829,40 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
return true;
}
}
/**
* Value object extension to {@link PropertyMatch} that matches for fields only for Kotlin data classes.
*
* @author Mark Paluch
* @since 3.2.8
*/
static class KotlinDataClassPropertyMatch extends PropertyMatch {
public KotlinDataClassPropertyMatch(@Nullable String namePattern, @Nullable String typeName) {
super(namePattern, typeName);
}
@Override
public boolean matches(Field field) {
if (!KotlinReflectionUtils.isDataClass(field.getDeclaringClass())) {
return false;
}
return super.matches(field);
}
@Override
public boolean matches(Property property) {
Field field = property.getField().orElse(null);
if (field == null || !KotlinReflectionUtils.isDataClass(field.getDeclaringClass())) {
return false;
}
return super.matches(property);
}
}
}
}

View File

@@ -39,7 +39,9 @@ import org.springframework.data.mapping.ShadowedPropertyType;
import org.springframework.data.mapping.ShadowedPropertyTypeWithCtor;
import org.springframework.data.mapping.ShadowingPropertyType;
import org.springframework.data.mapping.ShadowingPropertyTypeWithCtor;
import org.springframework.data.mapping.model.AbstractPersistentProperty;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.DataClassWithLazy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.StreamUtils;
import org.springframework.data.util.Streamable;
@@ -179,6 +181,16 @@ class AbstractMappingContextUnitTests {
assertThat(context.getPersistentEntity(SimpleDataClass.class)).isNotNull();
}
@Test // GH-3112
void shouldIgnoreLazyFieldsForDataClasses() {
BasicPersistentEntity<Object, SamplePersistentProperty> entity = context
.getRequiredPersistentEntity(DataClassWithLazy.class);
List<String> propertyNames = Streamable.of(entity).map(AbstractPersistentProperty::getName).toList();
assertThat(propertyNames).containsOnly("amount", "currency");
}
@Test // DATACMNS-1171
void shouldNotCreateEntityForSyntheticKotlinClass() {
assertThat(context.getPersistentEntity(TypeCreatingSyntheticClass.class)).isNotNull();

View File

@@ -29,6 +29,13 @@ data class ExtendedDataClassKt(val id: Long, val name: String) {
}
}
data class DataClassWithLazy(
val amount: Int,
val currency: String,
) {
val foo by lazy { 123 }
}
data class SingleSettableProperty constructor(val id: Double = Math.random()) {
val version: Int? = null
}