Skip to content

Commit 4a79e26

Browse files
committed
Polishing.
Extend tests. Document property overrides.
1 parent 0035e43 commit 4a79e26

File tree

3 files changed

+180
-17
lines changed

3 files changed

+180
-17
lines changed

src/main/asciidoc/object-mapping.adoc

+145-3
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ For that we use the following algorithm:
7676
1. If the property is immutable but exposes a `with…` method (see below), we use the `with…` method to create a new entity instance with the new property value.
7777
2. If property access (i.e. access through getters and setters) is defined, we're invoking the setter method.
7878
3. If the property is mutable we set the field directly.
79-
3. If the property is immutable we're using the constructor to be used by persistence operations (see <<mapping.object-creation>>) to create a copy of the instance.
80-
4. By default, we set the field value directly.
79+
4. If the property is immutable we're using the constructor to be used by persistence operations (see <<mapping.object-creation>>) to create a copy of the instance.
80+
5. By default, we set the field value directly.
8181

8282
[[mapping.property-population.details]]
8383
.Property population internals
@@ -146,7 +146,7 @@ For the domain class to be eligible for such optimization, it needs to adhere to
146146
- Types must not reside in the default or under the `java` package.
147147
- Types and their constructors must be `public`
148148
- Types that are inner classes must be `static`.
149-
- The used Java Runtime must allow for declaring classes in the originating `ClassLoader`. Java 9 and newer impose certain limitations.
149+
- The used Java Runtime must allow for declaring classes in the originating `ClassLoader`.Java 9 and newer impose certain limitations.
150150
151151
By default, Spring Data attempts to use generated property accessors and falls back to reflection-based ones if a limitation is detected.
152152
****
@@ -221,6 +221,73 @@ It's an established pattern to rather use static factory methods to expose these
221221
* _For identifiers to be generated, still use a final field in combination with an all-arguments persistence constructor (preferred) or a `with…` method_ --
222222
* _Use Lombok to avoid boilerplate code_ -- As persistence operations usually require a constructor taking all arguments, their declaration becomes a tedious repetition of boilerplate parameter to field assignments that can best be avoided by using Lombok's `@AllArgsConstructor`.
223223

224+
[[mapping.general-recommendations.override.properties]]
225+
=== Overriding Properties
226+
227+
Java's allows a flexible design of domain classes where a subclass could define a property that is already declared with the same name in its superclass.
228+
Consider the following example:
229+
230+
====
231+
[source,java]
232+
----
233+
public class SuperType {
234+
235+
private CharSequence field;
236+
237+
public SuperType(CharSequence field) {
238+
this.field = field;
239+
}
240+
241+
public CharSequence getField() {
242+
return this.field;
243+
}
244+
245+
public void setField(CharSequence field) {
246+
this.field = field;
247+
}
248+
}
249+
250+
public class SubType extends SuperType {
251+
252+
private String field;
253+
254+
public SubType(String field) {
255+
super(field);
256+
this.field = field;
257+
}
258+
259+
@Override
260+
public String getField() {
261+
return this.field;
262+
}
263+
264+
public void setField(String field) {
265+
this.field = field;
266+
267+
// optional
268+
super.setField(field);
269+
}
270+
}
271+
----
272+
====
273+
274+
Both classes define a `field` using assignable types. `SubType` however shadows `SuperType.field`.
275+
Depending on the class design, using the constructor could be the only default approach to set `SuperType.field`.
276+
Alternatively, calling `super.setField(…)` in the setter could set the `field` in `SuperType`.
277+
All these mechanisms create conflicts to some degree because the properties share the same name yet might represent two distinct values.
278+
Spring Data skips automatically super-type properties if types are not assignable.
279+
That is, the type of the overridden property must be assignable to its super-type property type to be registered as override, otherwise the super-type property is considered transient.
280+
We generally recommend using distinct property names.
281+
282+
Spring Data modules generally support overridden properties holding different values.
283+
From a programming model perspective there are a few things to consider:
284+
285+
1. Which property should be persisted (default to all declared properties)?
286+
You can exclude properties by annotating these with `@Transient`.
287+
2. How to represent properties in your data store?
288+
Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name.
289+
3. Using `@AccessType(PROPERTY)` cannot be used as the super-property cannot be generally set without making any further assumptions of the setter implementation.
290+
224291
[[mapping.kotlin]]
225292
== Kotlin support
226293

@@ -277,3 +344,78 @@ data class Person(val id: String, val name: String)
277344

278345
This class is effectively immutable.
279346
It allows creating new instances as Kotlin generates a `copy(…)` method that creates new object instances copying all property values from the existing object and applying property values provided as arguments to the method.
347+
348+
[[mapping.kotlin.override.properties]]
349+
=== Kotlin Overriding Properties
350+
351+
Kotlin allows declaring https://kotlinlang.org/docs/inheritance.html#overriding-properties[property overrides] to alter properties in subclasses.
352+
353+
====
354+
[source,kotlin]
355+
----
356+
open class SuperType(open var field: Int)
357+
358+
class SubType(override var field: Int = 1) :
359+
SuperType(field) {
360+
}
361+
----
362+
====
363+
364+
Such an arrangement renders two properties with the name `field`.
365+
Kotlin generates property accessors (getters and setters) for each property in each class.
366+
Effectively, the code looks like as follows:
367+
368+
====
369+
[source,java]
370+
----
371+
public class SuperType {
372+
373+
private int field;
374+
375+
public SuperType(int field) {
376+
this.field = field;
377+
}
378+
379+
public int getField() {
380+
return this.field;
381+
}
382+
383+
public void setField(int field) {
384+
this.field = field;
385+
}
386+
}
387+
388+
public final class SubType extends SuperType {
389+
390+
private int field;
391+
392+
public SubType(int field) {
393+
super(field);
394+
this.field = field;
395+
}
396+
397+
public int getField() {
398+
return this.field;
399+
}
400+
401+
public void setField(int field) {
402+
this.field = field;
403+
}
404+
}
405+
----
406+
====
407+
408+
Getters and setters on `SubType` set only `SubType.field` and not `SuperType.field`.
409+
In such an arrangement, using the constructor is the only default approach to set `SuperType.field`.
410+
Adding a method to `SubType` to set `SuperType.field` via `this.SuperType.field = …` is possible but falls outside of supported conventions.
411+
Property overrides create conflicts to some degree because the properties share the same name yet might represent two distinct values.
412+
We generally recommend using distinct property names.
413+
414+
Spring Data modules generally support overridden properties holding different values.
415+
From a programming model perspective there are a few things to consider:
416+
417+
1. Which property should be persisted (default to all declared properties)?
418+
You can exclude properties by annotating these with `@Transient`.
419+
2. How to represent properties in your data store?
420+
Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name.
421+
3. Using `@AccessType(PROPERTY)` cannot be used as the super-property cannot be set.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ protected boolean shouldSkipOverrideProperty(P property) {
586586
Class<?> propertyType = getPropertyType(property);
587587
Class<?> existingPropertyType = getPropertyType(existingProperty);
588588

589-
if (!existingPropertyType.isAssignableFrom(propertyType)) {
589+
if (!propertyType.isAssignableFrom(existingPropertyType)) {
590590

591591
if (LOGGER.isDebugEnabled()) {
592592
LOGGER.warn(String.format("Offending property declaration in '%s %s.%s' shadowing '%s %s.%s' in '%s'. ",

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

+34-13
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ void cleansUpCacheForRuntimeException() {
223223
.isThrownBy(() -> context.getPersistentEntity(Unsupported.class));
224224
}
225225

226-
@Test // DATACMNS-1509
226+
@Test // GH-3113
227227
void shouldIgnoreKotlinOverrideCtorPropertyInSuperClass() {
228228

229229
BasicPersistentEntity<Object, SamplePersistentProperty> entity = context
@@ -234,7 +234,7 @@ void shouldIgnoreKotlinOverrideCtorPropertyInSuperClass() {
234234
});
235235
}
236236

237-
@Test // DATACMNS-1509
237+
@Test // GH-3113
238238
void shouldIncludeAssignableKotlinOverridePropertyInSuperClass() {
239239

240240
BasicPersistentEntity<Object, SamplePersistentProperty> entity = context
@@ -244,19 +244,26 @@ void shouldIncludeAssignableKotlinOverridePropertyInSuperClass() {
244244
});
245245
}
246246

247-
@Test // DATACMNS-1509
247+
@Test // GH-3113
248248
void shouldIncludeAssignableShadowedPropertyInSuperClass() {
249249

250250
BasicPersistentEntity<Object, SamplePersistentProperty> entity = context
251-
.getPersistentEntity(ClassTypeInformation.from(ShadowingProperty.class));
251+
.getPersistentEntity(ClassTypeInformation.from(ShadowingPropertyAssignable.class));
252252

253253
assertThat(StreamUtils.createStreamFromIterator(entity.iterator())
254-
.filter(it -> it.getField().getDeclaringClass().equals(ShadowedProperty.class)).findFirst() //
254+
.filter(it -> it.getField().getDeclaringClass().equals(ShadowedPropertyAssignable.class)).findFirst() //
255255
).isNotEmpty();
256+
257+
assertThat(entity).hasSize(2);
258+
259+
entity.doWithProperties((PropertyHandler<SamplePersistentProperty>) property -> {
260+
assertThat(property.getField().getDeclaringClass()).isIn(ShadowedPropertyAssignable.class,
261+
ShadowingPropertyAssignable.class);
262+
});
256263
}
257264

258-
@Test // DATACMNS-1509
259-
void shouldIgnoreOverridePropertyInSuperClass() {
265+
@Test // GH-3113
266+
void shouldIgnoreNonAssignableOverridePropertyInSuperClass() {
260267

261268
BasicPersistentEntity<Object, SamplePersistentProperty> entity = context
262269
.getPersistentEntity(ClassTypeInformation.from(ShadowingPropertyNotAssignable.class));
@@ -391,23 +398,37 @@ public String getValue() {
391398
static class ShadowedPropertyNotAssignable {
392399

393400
private String value;
394-
395401
}
396402

397403
static class ShadowingPropertyNotAssignable extends ShadowedPropertyNotAssignable {
398404

399-
private int value;
405+
private Integer value;
400406

401-
ShadowingPropertyNotAssignable(int value) {
407+
ShadowingPropertyNotAssignable(Integer value) {
402408
this.value = value;
403409
}
404410

405-
public void setValue(int value) {
411+
public Integer getValue() {
412+
return value;
413+
}
414+
415+
public void setValue(Integer value) {
406416
this.value = value;
407417
}
418+
}
408419

409-
public int getValue() {
410-
return value;
420+
static class ShadowedPropertyAssignable {
421+
422+
private Object value;
423+
424+
}
425+
426+
static class ShadowingPropertyAssignable extends ShadowedPropertyAssignable {
427+
428+
private Integer value;
429+
430+
ShadowingPropertyAssignable(Integer value) {
431+
this.value = value;
411432
}
412433
}
413434

0 commit comments

Comments
 (0)