Skip to content

Commit dc82813

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 292ab23 commit dc82813

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
@@ -726,6 +726,7 @@ static enum PersistentPropertyFilter implements FieldFilter {
726726
matches.add(new PropertyMatch("class", null));
727727
matches.add(new PropertyMatch("this\\$.*", null));
728728
matches.add(new PropertyMatch("metaClass", "groovy.lang.MetaClass"));
729+
matches.add(new KotlinDataClassPropertyMatch(".*\\$delegate", null));
729730

730731
UNMAPPED_PROPERTIES = Streamable.of(matches);
731732
}
@@ -738,7 +739,7 @@ public boolean matches(Field field) {
738739
}
739740

740741
return UNMAPPED_PROPERTIES.stream()//
741-
.noneMatch(it -> it.matches(field.getName(), field.getType()));
742+
.noneMatch(it -> it.matches(field));
742743
}
743744

744745
/**
@@ -756,7 +757,7 @@ public boolean matches(Property property) {
756757
}
757758

758759
return UNMAPPED_PROPERTIES.stream()//
759-
.noneMatch(it -> it.matches(property.getName(), property.getType()));
760+
.noneMatch(it -> it.matches(property));
760761
}
761762

762763
/**
@@ -788,6 +789,26 @@ public PropertyMatch(@Nullable String namePattern, @Nullable String typeName) {
788789
/**
789790
* Returns whether the given {@link Field} matches the defined {@link PropertyMatch}.
790791
*
792+
* @param field must not be {@literal null}.
793+
* @return
794+
*/
795+
public boolean matches(Field field) {
796+
return matches(field.getName(), field.getType());
797+
}
798+
799+
/**
800+
* Returns whether the given {@link Property} matches the defined {@link PropertyMatch}.
801+
*
802+
* @param property must not be {@literal null}.
803+
* @return
804+
*/
805+
public boolean matches(Property property) {
806+
return matches(property.getName(), property.getType());
807+
}
808+
809+
/**
810+
* Returns whether the given field name and type matches the defined {@link PropertyMatch}.
811+
*
791812
* @param name must not be {@literal null}.
792813
* @param type must not be {@literal null}.
793814
* @return
@@ -808,5 +829,40 @@ public boolean matches(String name, Class<?> type) {
808829
return true;
809830
}
810831
}
832+
833+
/**
834+
* Value object extension to {@link PropertyMatch} that matches for fields only for Kotlin data classes.
835+
*
836+
* @author Mark Paluch
837+
* @since 3.2.8
838+
*/
839+
static class KotlinDataClassPropertyMatch extends PropertyMatch {
840+
841+
public KotlinDataClassPropertyMatch(@Nullable String namePattern, @Nullable String typeName) {
842+
super(namePattern, typeName);
843+
}
844+
845+
@Override
846+
public boolean matches(Field field) {
847+
848+
if (!KotlinReflectionUtils.isDataClass(field.getDeclaringClass())) {
849+
return false;
850+
}
851+
852+
return super.matches(field);
853+
}
854+
855+
@Override
856+
public boolean matches(Property property) {
857+
858+
Field field = property.getField().orElse(null);
859+
860+
if (field == null || !KotlinReflectionUtils.isDataClass(field.getDeclaringClass())) {
861+
return false;
862+
}
863+
864+
return super.matches(property);
865+
}
866+
}
811867
}
812868
}

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)