Fall back to canonical constructor in constructor resolution when using Java Records.
We now fall back to the canonical constructor of records when we cannot disambiguate which constructor to use. Also, reflect the Kotlin persistence creator mechanism. Closes #2625 Original pull request: #2694.
This commit is contained in:
@@ -20,7 +20,8 @@ The resolution algorithm works as follows:
|
||||
1. If there is a single static factory method annotated with `@PersistenceCreator` then it is used.
|
||||
2. If there is a single constructor, it is used.
|
||||
3. If there are multiple constructors and exactly one is annotated with `@PersistenceCreator`, it is used.
|
||||
4. If there's a no-argument constructor, it is used.
|
||||
4. If the type is a Java `Record` the canonical constructor is used.
|
||||
5. If there's a no-argument constructor, it is used.
|
||||
Other constructors will be ignored.
|
||||
|
||||
The value resolution assumes constructor/factory method argument names to match the property names of the entity, i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.).
|
||||
@@ -295,9 +296,23 @@ Using the same field/column name for different values typically leads to corrupt
|
||||
|
||||
Spring Data adapts specifics of Kotlin to allow object creation and mutation.
|
||||
|
||||
[[mapping.kotlin.creation]]
|
||||
=== Kotlin object creation
|
||||
|
||||
Kotlin classes are supported to be instantiated , all classes are immutable by default and require explicit property declarations to define mutable properties.
|
||||
Kotlin classes are supported to be instantiated, all classes are immutable by default and require explicit property declarations to define mutable properties.
|
||||
|
||||
Spring Data automatically tries to detect a persistent entity's constructor to be used to materialize objects of that type.
|
||||
The resolution algorithm works as follows:
|
||||
|
||||
1. If there is a constructor that is annotated with `@PersistenceCreator`, it is used.
|
||||
2. If the type is a <<mapping.kotlin,Kotlin data cass>> the primary constructor is used.
|
||||
3. If there is a single static factory method annotated with `@PersistenceCreator` then it is used.
|
||||
4. If there is a single constructor, it is used.
|
||||
5. If there are multiple constructors and exactly one is annotated with `@PersistenceCreator`, it is used.
|
||||
6. If the type is a Java `Record` the canonical constructor is used.
|
||||
7. If there's a no-argument constructor, it is used.
|
||||
Other constructors will be ignored.
|
||||
|
||||
Consider the following `data` class `Person`:
|
||||
|
||||
====
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.springframework.data.mapping.model;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.data.annotation.PersistenceConstructor;
|
||||
import org.springframework.data.mapping.InstanceCreatorMetadata;
|
||||
import org.springframework.data.mapping.Parameter;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
@@ -28,9 +27,10 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link PersistentPropertyAccessor} that will use an entity's {@link PersistenceConstructor} to create a new
|
||||
* instance of it to apply a new value for a given {@link PersistentProperty}. Will only be used if the
|
||||
* {@link PersistentProperty} is to be applied on a completely immutable entity type exposing a persistence constructor.
|
||||
* A {@link PersistentPropertyAccessor} that will use an entity's
|
||||
* {@link org.springframework.data.annotation.PersistenceCreator} to create a new instance of it to apply a new value
|
||||
* for a given {@link PersistentProperty}. Will only be used if the {@link PersistentProperty} is to be applied on a
|
||||
* completely immutable entity type exposing a entity creator.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @author Mark Paluch
|
||||
@@ -91,8 +91,7 @@ public class InstantiationAwarePropertyAccessor<T> implements PersistentProperty
|
||||
}
|
||||
|
||||
if (!creator.isCreatorParameter(property)) {
|
||||
throw new IllegalStateException(
|
||||
String.format(NO_CONSTRUCTOR_PARAMETER, property.getName(), creator));
|
||||
throw new IllegalStateException(String.format(NO_CONSTRUCTOR_PARAMETER, property.getName(), creator));
|
||||
}
|
||||
|
||||
creator.getParameters().forEach(it -> {
|
||||
|
||||
@@ -22,6 +22,7 @@ import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -34,11 +35,11 @@ import org.springframework.data.mapping.Parameter;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PreferredConstructor;
|
||||
import org.springframework.data.util.ClassTypeInformation;
|
||||
import org.springframework.data.util.KotlinReflectionUtils;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Helper class to find a {@link PreferredConstructor}.
|
||||
@@ -49,7 +50,7 @@ import org.springframework.util.Assert;
|
||||
* @author Mark Paluch
|
||||
* @author Xeno Amess
|
||||
*/
|
||||
public interface PreferredConstructorDiscoverer<T, P extends PersistentProperty<P>> {
|
||||
public interface PreferredConstructorDiscoverer {
|
||||
|
||||
/**
|
||||
* Discovers the {@link PreferredConstructor} for the given type.
|
||||
@@ -124,12 +125,45 @@ public interface PreferredConstructorDiscoverer<T, P extends PersistentProperty<
|
||||
}
|
||||
}
|
||||
|
||||
if (rawOwningType.isRecord() && (candidates.size() > 1 || (noArg != null && !candidates.isEmpty()))) {
|
||||
return RECORD.discover(type, entity);
|
||||
}
|
||||
|
||||
if (noArg != null) {
|
||||
return buildPreferredConstructor(noArg, type, entity);
|
||||
}
|
||||
|
||||
return candidates.size() > 1 || candidates.isEmpty() ? null
|
||||
: buildPreferredConstructor(candidates.iterator().next(), type, entity);
|
||||
if (candidates.size() == 1) {
|
||||
return buildPreferredConstructor(candidates.iterator().next(), type, entity);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Discovers the canonical constructor for Java Record types.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
RECORD {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
<T, P extends PersistentProperty<P>> PreferredConstructor<T, P> discover(TypeInformation<T> type,
|
||||
@Nullable PersistentEntity<T, P> entity) {
|
||||
|
||||
Class<?> rawOwningType = type.getType();
|
||||
|
||||
if (!rawOwningType.isRecord()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Class<?>[] paramTypes = Arrays.stream(rawOwningType.getRecordComponents()).map(RecordComponent::getType)
|
||||
.toArray(Class<?>[]::new);
|
||||
Constructor<?> canonicalConstructor = ClassUtils.getConstructorIfAvailable(rawOwningType, paramTypes);
|
||||
|
||||
return canonicalConstructor != null ? buildPreferredConstructor(canonicalConstructor, type, entity) : null;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*;
|
||||
import lombok.Value;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.data.mapping.context.SampleMappingContext;
|
||||
import org.springframework.data.mapping.context.SamplePersistentProperty;
|
||||
import org.springframework.data.mapping.model.EntityInstantiators;
|
||||
@@ -44,8 +45,7 @@ class InstantiationAwarePersistentPropertyAccessorUnitTests {
|
||||
var sample = new Sample("Dave", "Matthews", 42);
|
||||
|
||||
PersistentPropertyAccessor<Sample> wrapper = new InstantiationAwarePropertyAccessor<>(sample,
|
||||
entity::getPropertyAccessor,
|
||||
instantiators);
|
||||
entity::getPropertyAccessor, instantiators);
|
||||
|
||||
wrapper.setProperty(entity.getRequiredPersistentProperty("firstname"), "Oliver August");
|
||||
|
||||
@@ -71,10 +71,38 @@ class InstantiationAwarePersistentPropertyAccessorUnitTests {
|
||||
assertThat(wrapper.getBean()).isEqualTo(new Sample("Oliver August", "Heisenberg", 42));
|
||||
}
|
||||
|
||||
@Test // GH-2625
|
||||
void shouldSetPropertyOfRecordUsingCanonicalConstructor() {
|
||||
|
||||
var instantiators = new EntityInstantiators();
|
||||
var context = new SampleMappingContext();
|
||||
|
||||
PersistentEntity<Object, SamplePersistentProperty> entity = context
|
||||
.getRequiredPersistentEntity(WithSingleArgConstructor.class);
|
||||
|
||||
var bean = new WithSingleArgConstructor(42L, "Dave");
|
||||
|
||||
PersistentPropertyAccessor<WithSingleArgConstructor> wrapper = new InstantiationAwarePropertyAccessor<>(bean,
|
||||
entity::getPropertyAccessor, instantiators);
|
||||
|
||||
wrapper.setProperty(entity.getRequiredPersistentProperty("name"), "Oliver August");
|
||||
wrapper.setProperty(entity.getRequiredPersistentProperty("id"), 41L);
|
||||
|
||||
assertThat(wrapper.getBean()).isEqualTo(new WithSingleArgConstructor(41L, "Oliver August"));
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Sample {
|
||||
|
||||
String firstname, lastname;
|
||||
int age;
|
||||
}
|
||||
|
||||
public record WithSingleArgConstructor(Long id, String name) {
|
||||
|
||||
public WithSingleArgConstructor(String name) {
|
||||
this(null, name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.Iterator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.annotation.PersistenceConstructor;
|
||||
import org.springframework.data.annotation.PersistenceCreator;
|
||||
import org.springframework.data.mapping.PreferredConstructorDiscovererUnitTests.Outer.Inner;
|
||||
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
||||
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
|
||||
@@ -149,6 +150,34 @@ class PreferredConstructorDiscovererUnitTests<P extends PersistentProperty<P>> {
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-2332
|
||||
void detectsCanonicalRecordConstructor() {
|
||||
|
||||
assertThat(PreferredConstructorDiscoverer.discover(RecordWithSingleArgConstructor.class)).satisfies(ctor -> {
|
||||
|
||||
assertThat(ctor.getParameters()).hasSize(2);
|
||||
assertThat(ctor.getParameters().get(0).getRawType()).isEqualTo(Long.class);
|
||||
assertThat(ctor.getParameters().get(1).getRawType()).isEqualTo(String.class);
|
||||
});
|
||||
|
||||
assertThat(PreferredConstructorDiscoverer.discover(RecordWithNoArgConstructor.class)).satisfies(ctor -> {
|
||||
|
||||
assertThat(ctor.getParameters()).hasSize(2);
|
||||
assertThat(ctor.getParameters().get(0).getRawType()).isEqualTo(Long.class);
|
||||
assertThat(ctor.getParameters().get(1).getRawType()).isEqualTo(String.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-2332
|
||||
void detectsAnnotatedRecordConstructor() {
|
||||
|
||||
assertThat(PreferredConstructorDiscoverer.discover(RecordWithPersistenceCreator.class)).satisfies(ctor -> {
|
||||
|
||||
assertThat(ctor.getParameters()).hasSize(1);
|
||||
assertThat(ctor.getParameters().get(0).getRawType()).isEqualTo(String.class);
|
||||
});
|
||||
}
|
||||
|
||||
static class SyntheticConstructor {
|
||||
@PersistenceConstructor
|
||||
private SyntheticConstructor(String x) {}
|
||||
@@ -227,5 +256,28 @@ class PreferredConstructorDiscovererUnitTests<P extends PersistentProperty<P>> {
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Value("${hello-world}")
|
||||
@interface MyValue {}
|
||||
@interface MyValue {
|
||||
}
|
||||
|
||||
public record RecordWithSingleArgConstructor(Long id, String name) {
|
||||
|
||||
public RecordWithSingleArgConstructor(String name) {
|
||||
this(null, name);
|
||||
}
|
||||
}
|
||||
|
||||
public record RecordWithNoArgConstructor(Long id, String name) {
|
||||
|
||||
public RecordWithNoArgConstructor(String name) {
|
||||
this(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record RecordWithPersistenceCreator(Long id, String name) {
|
||||
|
||||
@PersistenceCreator
|
||||
public RecordWithPersistenceCreator(String name) {
|
||||
this(null, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user