Skip to content

Commit 6a0a404

Browse files
committed
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.
1 parent e003edc commit 6a0a404

File tree

5 files changed

+143
-15
lines changed

5 files changed

+143
-15
lines changed

src/main/asciidoc/object-mapping.adoc

+17-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ The resolution algorithm works as follows:
2020
1. If there is a single static factory method annotated with `@PersistenceCreator` then it is used.
2121
2. If there is a single constructor, it is used.
2222
3. If there are multiple constructors and exactly one is annotated with `@PersistenceCreator`, it is used.
23-
4. If there's a no-argument constructor, it is used.
23+
4. If the type is a Java `Record` the canonical constructor is used.
24+
5. If there's a no-argument constructor, it is used.
2425
Other constructors will be ignored.
2526

2627
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
295296

296297
Spring Data adapts specifics of Kotlin to allow object creation and mutation.
297298

299+
[[mapping.kotlin.creation]]
298300
=== Kotlin object creation
299301

300-
Kotlin classes are supported to be instantiated , all classes are immutable by default and require explicit property declarations to define mutable properties.
302+
Kotlin classes are supported to be instantiated, all classes are immutable by default and require explicit property declarations to define mutable properties.
303+
304+
Spring Data automatically tries to detect a persistent entity's constructor to be used to materialize objects of that type.
305+
The resolution algorithm works as follows:
306+
307+
1. If there is a constructor that is annotated with `@PersistenceCreator`, it is used.
308+
2. If the type is a <<mapping.kotlin,Kotlin data cass>> the primary constructor is used.
309+
3. If there is a single static factory method annotated with `@PersistenceCreator` then it is used.
310+
4. If there is a single constructor, it is used.
311+
5. If there are multiple constructors and exactly one is annotated with `@PersistenceCreator`, it is used.
312+
6. If the type is a Java `Record` the canonical constructor is used.
313+
7. If there's a no-argument constructor, it is used.
314+
Other constructors will be ignored.
315+
301316
Consider the following `data` class `Person`:
302317

303318
====

src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java

+5-6
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.util.function.Function;
1919

2020
import org.springframework.core.KotlinDetector;
21-
import org.springframework.data.annotation.PersistenceConstructor;
2221
import org.springframework.data.mapping.InstanceCreatorMetadata;
2322
import org.springframework.data.mapping.Parameter;
2423
import org.springframework.data.mapping.PersistentEntity;
@@ -28,9 +27,10 @@
2827
import org.springframework.util.Assert;
2928

3029
/**
31-
* A {@link PersistentPropertyAccessor} that will use an entity's {@link PersistenceConstructor} to create a new
32-
* instance of it to apply a new value for a given {@link PersistentProperty}. Will only be used if the
33-
* {@link PersistentProperty} is to be applied on a completely immutable entity type exposing a persistence constructor.
30+
* A {@link PersistentPropertyAccessor} that will use an entity's
31+
* {@link org.springframework.data.annotation.PersistenceCreator} to create a new instance of it to apply a new value
32+
* for a given {@link PersistentProperty}. Will only be used if the {@link PersistentProperty} is to be applied on a
33+
* completely immutable entity type exposing a entity creator.
3434
*
3535
* @author Oliver Drotbohm
3636
* @author Mark Paluch
@@ -91,8 +91,7 @@ public void setProperty(PersistentProperty<?> property, @Nullable Object value)
9191
}
9292

9393
if (!creator.isCreatorParameter(property)) {
94-
throw new IllegalStateException(
95-
String.format(NO_CONSTRUCTOR_PARAMETER, property.getName(), creator));
94+
throw new IllegalStateException(String.format(NO_CONSTRUCTOR_PARAMETER, property.getName(), creator));
9695
}
9796

9897
creator.getParameters().forEach(it -> {

src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java

+38-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import java.lang.annotation.Annotation;
2424
import java.lang.reflect.Constructor;
25+
import java.lang.reflect.RecordComponent;
2526
import java.util.ArrayList;
2627
import java.util.Arrays;
2728
import java.util.List;
@@ -34,11 +35,11 @@
3435
import org.springframework.data.mapping.PersistentEntity;
3536
import org.springframework.data.mapping.PersistentProperty;
3637
import org.springframework.data.mapping.PreferredConstructor;
37-
import org.springframework.data.util.ClassTypeInformation;
3838
import org.springframework.data.util.KotlinReflectionUtils;
3939
import org.springframework.data.util.TypeInformation;
4040
import org.springframework.lang.Nullable;
4141
import org.springframework.util.Assert;
42+
import org.springframework.util.ClassUtils;
4243

4344
/**
4445
* Helper class to find a {@link PreferredConstructor}.
@@ -49,7 +50,7 @@
4950
* @author Mark Paluch
5051
* @author Xeno Amess
5152
*/
52-
public interface PreferredConstructorDiscoverer<T, P extends PersistentProperty<P>> {
53+
public interface PreferredConstructorDiscoverer {
5354

5455
/**
5556
* Discovers the {@link PreferredConstructor} for the given type.
@@ -124,12 +125,45 @@ <T, P extends PersistentProperty<P>> PreferredConstructor<T, P> discover(TypeInf
124125
}
125126
}
126127

128+
if (rawOwningType.isRecord() && (candidates.size() > 1 || (noArg != null && !candidates.isEmpty()))) {
129+
return RECORD.discover(type, entity);
130+
}
131+
127132
if (noArg != null) {
128133
return buildPreferredConstructor(noArg, type, entity);
129134
}
130135

131-
return candidates.size() > 1 || candidates.isEmpty() ? null
132-
: buildPreferredConstructor(candidates.iterator().next(), type, entity);
136+
if (candidates.size() == 1) {
137+
return buildPreferredConstructor(candidates.iterator().next(), type, entity);
138+
}
139+
140+
return null;
141+
}
142+
},
143+
144+
/**
145+
* Discovers the canonical constructor for Java Record types.
146+
*
147+
* @since 3.0
148+
*/
149+
RECORD {
150+
151+
@Nullable
152+
@Override
153+
<T, P extends PersistentProperty<P>> PreferredConstructor<T, P> discover(TypeInformation<T> type,
154+
@Nullable PersistentEntity<T, P> entity) {
155+
156+
Class<?> rawOwningType = type.getType();
157+
158+
if (!rawOwningType.isRecord()) {
159+
return null;
160+
}
161+
162+
Class<?>[] paramTypes = Arrays.stream(rawOwningType.getRecordComponents()).map(RecordComponent::getType)
163+
.toArray(Class<?>[]::new);
164+
Constructor<?> canonicalConstructor = ClassUtils.getConstructorIfAvailable(rawOwningType, paramTypes);
165+
166+
return canonicalConstructor != null ? buildPreferredConstructor(canonicalConstructor, type, entity) : null;
133167
}
134168
},
135169

src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java

+30-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import lombok.Value;
2121

2222
import org.junit.jupiter.api.Test;
23+
2324
import org.springframework.data.mapping.context.SampleMappingContext;
2425
import org.springframework.data.mapping.context.SamplePersistentProperty;
2526
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -44,8 +45,7 @@ void shouldCreateNewInstance() {
4445
var sample = new Sample("Dave", "Matthews", 42);
4546

4647
PersistentPropertyAccessor<Sample> wrapper = new InstantiationAwarePropertyAccessor<>(sample,
47-
entity::getPropertyAccessor,
48-
instantiators);
48+
entity::getPropertyAccessor, instantiators);
4949

5050
wrapper.setProperty(entity.getRequiredPersistentProperty("firstname"), "Oliver August");
5151

@@ -71,10 +71,38 @@ void shouldSetMultipleProperties() {
7171
assertThat(wrapper.getBean()).isEqualTo(new Sample("Oliver August", "Heisenberg", 42));
7272
}
7373

74+
@Test // GH-2625
75+
void shouldSetPropertyOfRecordUsingCanonicalConstructor() {
76+
77+
var instantiators = new EntityInstantiators();
78+
var context = new SampleMappingContext();
79+
80+
PersistentEntity<Object, SamplePersistentProperty> entity = context
81+
.getRequiredPersistentEntity(WithSingleArgConstructor.class);
82+
83+
var bean = new WithSingleArgConstructor(42L, "Dave");
84+
85+
PersistentPropertyAccessor<WithSingleArgConstructor> wrapper = new InstantiationAwarePropertyAccessor<>(bean,
86+
entity::getPropertyAccessor, instantiators);
87+
88+
wrapper.setProperty(entity.getRequiredPersistentProperty("name"), "Oliver August");
89+
wrapper.setProperty(entity.getRequiredPersistentProperty("id"), 41L);
90+
91+
assertThat(wrapper.getBean()).isEqualTo(new WithSingleArgConstructor(41L, "Oliver August"));
92+
}
93+
7494
@Value
7595
static class Sample {
7696

7797
String firstname, lastname;
7898
int age;
7999
}
100+
101+
public record WithSingleArgConstructor(Long id, String name) {
102+
103+
public WithSingleArgConstructor(String name) {
104+
this(null, name);
105+
}
106+
}
107+
80108
}

src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java

+53-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.junit.jupiter.api.Test;
2727
import org.springframework.beans.factory.annotation.Value;
2828
import org.springframework.data.annotation.PersistenceConstructor;
29+
import org.springframework.data.annotation.PersistenceCreator;
2930
import org.springframework.data.mapping.PreferredConstructorDiscovererUnitTests.Outer.Inner;
3031
import org.springframework.data.mapping.model.BasicPersistentEntity;
3132
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
@@ -149,6 +150,34 @@ void detectsMetaAnnotatedValueAnnotation() {
149150
});
150151
}
151152

153+
@Test // GH-2332
154+
void detectsCanonicalRecordConstructor() {
155+
156+
assertThat(PreferredConstructorDiscoverer.discover(RecordWithSingleArgConstructor.class)).satisfies(ctor -> {
157+
158+
assertThat(ctor.getParameters()).hasSize(2);
159+
assertThat(ctor.getParameters().get(0).getRawType()).isEqualTo(Long.class);
160+
assertThat(ctor.getParameters().get(1).getRawType()).isEqualTo(String.class);
161+
});
162+
163+
assertThat(PreferredConstructorDiscoverer.discover(RecordWithNoArgConstructor.class)).satisfies(ctor -> {
164+
165+
assertThat(ctor.getParameters()).hasSize(2);
166+
assertThat(ctor.getParameters().get(0).getRawType()).isEqualTo(Long.class);
167+
assertThat(ctor.getParameters().get(1).getRawType()).isEqualTo(String.class);
168+
});
169+
}
170+
171+
@Test // GH-2332
172+
void detectsAnnotatedRecordConstructor() {
173+
174+
assertThat(PreferredConstructorDiscoverer.discover(RecordWithPersistenceCreator.class)).satisfies(ctor -> {
175+
176+
assertThat(ctor.getParameters()).hasSize(1);
177+
assertThat(ctor.getParameters().get(0).getRawType()).isEqualTo(String.class);
178+
});
179+
}
180+
152181
static class SyntheticConstructor {
153182
@PersistenceConstructor
154183
private SyntheticConstructor(String x) {}
@@ -227,5 +256,28 @@ static class ClassWithMetaAnnotatedParameter {
227256
@Target(ElementType.PARAMETER)
228257
@Retention(RetentionPolicy.RUNTIME)
229258
@Value("${hello-world}")
230-
@interface MyValue {}
259+
@interface MyValue {
260+
}
261+
262+
public record RecordWithSingleArgConstructor(Long id, String name) {
263+
264+
public RecordWithSingleArgConstructor(String name) {
265+
this(null, name);
266+
}
267+
}
268+
269+
public record RecordWithNoArgConstructor(Long id, String name) {
270+
271+
public RecordWithNoArgConstructor(String name) {
272+
this(null, null);
273+
}
274+
}
275+
276+
public record RecordWithPersistenceCreator(Long id, String name) {
277+
278+
@PersistenceCreator
279+
public RecordWithPersistenceCreator(String name) {
280+
this(null, name);
281+
}
282+
}
231283
}

0 commit comments

Comments
 (0)