Skip to content

Commit a115ee6

Browse files
committed
Filter delegated properties for Kotlin data classes.
We now filter delegated properties (such as lazy) from being managed as persistent properties. Closes #3112
1 parent df2276a commit a115ee6

File tree

4 files changed

+81
-3
lines changed

4 files changed

+81
-3
lines changed

src/main/antora/modules/ROOT/pages/object-mapping.adoc

+4-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ Consider the following `data` class `Person`:
314314
data class Person(val id: String, val name: String)
315315
----
316316

317-
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:
317+
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:
318318

319319
[source,kotlin]
320320
----
@@ -335,6 +335,9 @@ data class Person(var id: String, val name: String = "unknown")
335335

336336
Every time the `name` parameter is either not part of the result or its value is `null`, then the `name` defaults to `unknown`.
337337

338+
NOTE: Delegated properties are not supported with Spring Data. The mapping metadata filters delegated properties for Kotlin Data classes.
339+
In all other cases you can exclude synthetic fields for delegated properties by annotating the property with `@delegate:org.springframework.data.annotation.Transient`.
340+
338341
[[property-population-of-kotlin-data-classes]]
339342
=== Property population of Kotlin data classes
340343

src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java

+58-2
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,7 @@ static enum PersistentPropertyFilter implements FieldFilter {
770770
matches.add(new PropertyMatch("class", null));
771771
matches.add(new PropertyMatch("this\\$.*", null));
772772
matches.add(new PropertyMatch("metaClass", "groovy.lang.MetaClass"));
773+
matches.add(new KotlinDataClassPropertyMatch(".*\\$delegate", null));
773774

774775
UNMAPPED_PROPERTIES = Streamable.of(matches);
775776
}
@@ -782,7 +783,7 @@ public boolean matches(Field field) {
782783
}
783784

784785
return UNMAPPED_PROPERTIES.stream()//
785-
.noneMatch(it -> it.matches(field.getName(), field.getType()));
786+
.noneMatch(it -> it.matches(field));
786787
}
787788

788789
/**
@@ -800,7 +801,7 @@ public boolean matches(Property property) {
800801
}
801802

802803
return UNMAPPED_PROPERTIES.stream()//
803-
.noneMatch(it -> it.matches(property.getName(), property.getType()));
804+
.noneMatch(it -> it.matches(property));
804805
}
805806

806807
/**
@@ -832,6 +833,26 @@ public PropertyMatch(@Nullable String namePattern, @Nullable String typeName) {
832833
/**
833834
* Returns whether the given {@link Field} matches the defined {@link PropertyMatch}.
834835
*
836+
* @param field must not be {@literal null}.
837+
* @return
838+
*/
839+
public boolean matches(Field field) {
840+
return matches(field.getName(), field.getType());
841+
}
842+
843+
/**
844+
* Returns whether the given {@link Property} matches the defined {@link PropertyMatch}.
845+
*
846+
* @param property must not be {@literal null}.
847+
* @return
848+
*/
849+
public boolean matches(Property property) {
850+
return matches(property.getName(), property.getType());
851+
}
852+
853+
/**
854+
* Returns whether the given field name and type matches the defined {@link PropertyMatch}.
855+
*
835856
* @param name must not be {@literal null}.
836857
* @param type must not be {@literal null}.
837858
* @return
@@ -852,6 +873,41 @@ public boolean matches(String name, Class<?> type) {
852873
return true;
853874
}
854875
}
876+
877+
/**
878+
* Value object extension to {@link PropertyMatch} that matches for fields only for Kotlin data classes.
879+
*
880+
* @author Mark Paluch
881+
* @since 3.3.2
882+
*/
883+
static class KotlinDataClassPropertyMatch extends PropertyMatch {
884+
885+
public KotlinDataClassPropertyMatch(@Nullable String namePattern, @Nullable String typeName) {
886+
super(namePattern, typeName);
887+
}
888+
889+
@Override
890+
public boolean matches(Field field) {
891+
892+
if (!KotlinReflectionUtils.isDataClass(field.getDeclaringClass())) {
893+
return false;
894+
}
895+
896+
return super.matches(field);
897+
}
898+
899+
@Override
900+
public boolean matches(Property property) {
901+
902+
Field field = property.getField().orElse(null);
903+
904+
if (field == null || !KotlinReflectionUtils.isDataClass(field.getDeclaringClass())) {
905+
return false;
906+
}
907+
908+
return super.matches(property);
909+
}
910+
}
855911
}
856912

857913
}

src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
import org.springframework.data.mapping.ShadowedPropertyTypeWithCtor;
4040
import org.springframework.data.mapping.ShadowingPropertyType;
4141
import org.springframework.data.mapping.ShadowingPropertyTypeWithCtor;
42+
import org.springframework.data.mapping.model.AbstractPersistentProperty;
4243
import org.springframework.data.mapping.model.BasicPersistentEntity;
44+
import org.springframework.data.mapping.model.DataClassWithLazy;
4345
import org.springframework.data.mapping.model.SimpleTypeHolder;
4446
import org.springframework.data.util.StreamUtils;
4547
import org.springframework.data.util.Streamable;
@@ -179,6 +181,16 @@ void shouldCreateEntityForKotlinDataClass() {
179181
assertThat(context.getPersistentEntity(SimpleDataClass.class)).isNotNull();
180182
}
181183

184+
@Test // GH-3112
185+
void shouldIgnoreLazyFieldsForDataClasses() {
186+
187+
BasicPersistentEntity<Object, SamplePersistentProperty> entity = context
188+
.getRequiredPersistentEntity(DataClassWithLazy.class);
189+
190+
List<String> propertyNames = Streamable.of(entity).map(AbstractPersistentProperty::getName).toList();
191+
assertThat(propertyNames).containsOnly("amount", "currency");
192+
}
193+
182194
@Test // DATACMNS-1171
183195
void shouldNotCreateEntityForSyntheticKotlinClass() {
184196
assertThat(context.getPersistentEntity(TypeCreatingSyntheticClass.class)).isNotNull();

src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt

+7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ data class ExtendedDataClassKt(val id: Long, val name: String) {
2929
}
3030
}
3131

32+
data class DataClassWithLazy(
33+
val amount: Int,
34+
val currency: String,
35+
) {
36+
val foo by lazy { 123 }
37+
}
38+
3239
data class SingleSettableProperty constructor(val id: Double = Math.random()) {
3340
val version: Int? = null
3441
}

0 commit comments

Comments
 (0)