This section covers the fundamentals of Spring Data object mapping, object creation, field and property access, mutability and immutability.
Note, that this section only applies to Spring Data modules that do not use the object mapping of the underlying data store (like JPA).
Also be sure to consult the store specific sections for store specific object mapping, like indexes, customizing columnd or field names or the like.
Also be sure to consult the store-specific sections for store-specific object mapping, like indexes, customizing column or field names or the like.
Core responsibility of the Spring Data object mapping is to create instances of domain objects and map the store-native data structures onto those.
This means we need two fundamental steps:
1. Instance creation by using one of the constructors exposed
1. Instance creation by using one of the constructors exposed.
2. Instance population to materialize all exposed properties.
[[mapping.object-creation]]
@ -62,7 +62,7 @@ For the domain class to be eligible for such optimization, it needs to adhere to
@@ -62,7 +62,7 @@ For the domain class to be eligible for such optimization, it needs to adhere to
- it must not be a CGLib proxy class
- the constructor to be used by Spring Data must not be private
If any of these criterias match, Spring Data will fall back to entity instantiation via reflection.
If any of these criteria match, Spring Data will fall back to entity instantiation via reflection.
****
[[mapping.property-population]]
@ -108,29 +108,45 @@ class Person {
@@ -108,29 +108,45 @@ class Person {
}
----
.A generated Property Accessor
====
[source, java]
----
class PersonPropertyAccessor implements PersistentPropertyAccessor {
private static final MethodHandle firstname; <2>
private static final MethodHandle firstname; <2>
private Person person; <1>
private Person person; <1>
public void setProperty(PersistentProperty property, Object value) {
String name = property.getName();
if ("firstname".equals(name)) {
firstname.…; <2>
firstname.invoke(person, (String) value); <2>
} else if ("id".equals(name)) {
this.person = person.withId((Long) value); <3>
this.person = person.withId((Long) value); <3>
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value); <4>
this.person.setLastname((String) value); <4>
}
}
}
----
<1> PropertyAccessor's hold a mutable instance of the underlying object. This is, to enable mutations of otherwise immutable properties.
<2> By default, Spring Data uses field-access to read and write property values. As per visibility rules of `private` fields, `MethodHandles` are used to interact with fields.
<3> The class exposes a `withId(…)` method that's used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated. Calling `withId(…)` creates a new `Person` object. All subsequent mutations will take place in the new instance leaving the previous untouched.
<4> Using property-access allows direct method invocations without using `MethodHandles`.
====
This gives us a roundabout 25% performance boost over reflection.
For the domain class to be eligible for such optimization, it needs to adhere to a set of constraints:
- Types must not reside in the default or under the `java` package.
- Types and their constructors must be `public`
- Types that are inner classes must be `static`.
- The used Java Runtime must allow for declaring classes in the originating `ClassLoader`. Java 9 and newer impose certain limitations.
By default, Spring Data attempts to use generated property accessors and falls back to reflection-based ones if a limitation is detected.
****
Let's have a look a
@ -141,13 +157,13 @@ Let's have a look a
@@ -141,13 +157,13 @@ Let's have a look a
static Person of(String firstname, String lastname, LocalDate birthday) { <6>
@ -164,11 +180,11 @@ class Person {
@@ -164,11 +180,11 @@ class Person {
this.age = age;
}
Person withId(Long id) { <1>
Person withId(Long id) { <1>
return new Person(id, this.firstname, this.lastname, this.birthday);
}
void setRemarks(String remarks) { <5>
void setRemarks(String remarks) { <5>
this.remarks = remarks;
}
}
@ -181,7 +197,7 @@ The same pattern is usually applied for other properties that are store managed
@@ -181,7 +197,7 @@ The same pattern is usually applied for other properties that are store managed
<2> The `firstname` and `lastname` properties are ordinary immutable properties potentially exposed through getters.
<3> The `age` property is an immutable but derived one from the `birthday` property.
With the design shown, the database value will trump the defaulting as Spring Data uses the only declared constructor.
Even if the intend is that the calculation should be preferred, it's important that this constructor also takes `age` as parameter (to potentially ignore it) as otherwise the property population step will attempt to set the age field and fail due to it being immutable and no wither being present.
Even if the intent is that the calculation should be preferred, it's important that this constructor also takes `age` as parameter (to potentially ignore it) as otherwise the property population step will attempt to set the age field and fail due to it being immutable and no wither being present.
<4> The `comment` property is mutable is populated by setting its field directly.
<5> The `remarks` properties are mutable and populated by setting the `comment` field directly or by invoking the setter method for
<6> The class exposes a factory method and a constructor for object creation.
@ -190,248 +206,22 @@ Instead, defaulting of properties is handled within the factory method.
@@ -190,248 +206,22 @@ Instead, defaulting of properties is handled within the factory method.
== General recommendations
* _Try to stick to immutable objects_ -- Immutable objects are straightforward to create as materializing an object is then a matter of calling its constructor only.
* _Try to stick to immutable objects_ -- Immutable objects are straightforward to create as materializing an object is then a matter of calling its constructor only.
Also, this avoids your domain objects to be littered with setter methods that allow client code to manipulate the objects state.
If you need those, prefer to make them package protected so that they can only be invoked by a limited amount of colocated types.
Constructor-only materialization is up to 30% faster than if we also have to populate properties.
If you need those, prefer to make them package protected so that they can only be invoked by a limited amount of co-located types.
Constructor-only materialization is up to 30% faster than properties population.
* _Provide an all-args constructor_ -- Even if you cannot or don't want to model your entities as immutable values, there's still value in providing a constructor that takes all properties of the entity as arguments, including the mutable ones, as this allows the object mapping to skip the property population for optimal performance.
* _Use factory methods instead of overloaded constructors to avoid ``@PersistenceConstructor``_ -- With an all-argument constructor needed for optimal performance, we usually want to expose more application use case specific constructors that omit things like auto-generated identifiers etc.
It's an established pattern to rather use static factory methods to expose these variants of the all-args constructor.
* _Make sure you adhere to the constraints that allow the generated instantiator and property accessor classes to be used_ --
* _For identifiers to be generated, still use a final field in combination with a wither method_ --
* _Use Lombok to avoid boilerplate code_ -- As persistence operations usually require a constructor taking all arguments, their declaration becomes a tedious repeatition of boilerplate parameter to field assignments that can best be avoided by using Lombok's `@AllArgsConstructor`.
== Old stuff
=== Immutable objects
Spring Data's support for complex constructors allows store data to be mapped to immutable objects and is the most effective way to populate
Object mutability impacts how Spring Data handles objects for specific functionality that requires Spring Data to change properties of a particular object.
Such changes can be Id generation, auditing or as simple as reading back an object.
In general, Spring Data has no visibility requirements for types, constructors or property accessors which allows you to design your data model according to your requirements.
Certain limitations may apply when using Spring Data on Java Runtimes that have encapsulation enabled.
[[mapping.mutability.mutable]]
== Mutable Objects
Mutable objects are objects whose properties are mutable.
Such objects have non-`final` fields and typically getters and setters. Consider the following class ``Person``:
====
[source,java]
----
class Person {
private String id;
private String name;
public void setId(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
// Other getters/setters omitted for brevity.
}
----
====
This class above is a typical example of a mutable object.
It has setters for fields. The class is created with a no-args constructor. When Spring Data needs to change a `Person` object then changes are applied in place by setting a property directly. This means that changes become visible in the object instance that was passed to Spring Data methods such as ``save(…)``. To avoid this behavior, let's take a look at <<mapping.mutability.immutable>>.
[[mapping.mutability.immutable]]
== Immutable Objects
Immutable objects are objects that do not allow changes to the actual object instance. Immutable objects can be such that entirely prevent association with to be updated property values or that create new instances.
Spring Data supports both flavors of immutable objects. Consider the following immutable class ``Person``:
====
[source,java]
----
class Person {
private final String id;
private final String name;
public Person(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
}
----
====
The `Person` class above is fully immutable - once it's created, it cannot be changed anymore. Object instances must be created by using the constructor which takes `id` and `name` parameters.
Spring Data's `EntityReader` is able to read immutable entities from a data store. The class above declares a single constructor. Read <<mapping.mutability.constructorcreation>> for further details on how to configure additional constructors.
Store modules that provide Id generation or auditing require to set the corresponding properties when persisting an object.
This isn't possible with a class like `Person` above. Attempts to set an immutable property result in `UnsupportedOperationException`.
To enable mutability with immutable objects, the class itself must declare methods that allow for creating object instances that hold all values from the previous instance and the updated property value. Spring Data supports the following patterns:
* Value objects exposing `with…` methods (Wither-methods)
* Usage of Kotlin data classes to leverage the `.copy(…)` method
=== With methods
Value objects providing `with…` methods create a new instance of an object that carries all previous property values and has a changed value of the `with…` property, as the following example shows:
====
[source,java]
----
class Person {
private final @Id String id;
private final String name;
public Person(String id, String name) {
this.id = id;
this.name = name;
}
public Person withId(String id) {
return new Person(id, this.name);
}
// other wither methods omitted for brevity.
}
----
====
NOTE: Immutable objects using wither methods create new instances on a `with…` call. Make sure to provide a constructor that takes all arguments to avoid excessive instantiations.
Lombok users can use `@Value` and `@Wither` annotations to follow the `with…` pattern.
=== Kotlin data classes
In Kotlin, all classes are immutable by default and require explicit property declarations to define mutable properties. Consider the following `data` class `Person`:
* _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`.
====
[source,java]
----
data class Person(val id: String, val name: String)
----
====
This class is effectively immutable. It allows to create 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.
[[mapping.mutability.propertyaccess]]
== Property Access
Spring Data attempts to use field access as the primary way how to retrieve and set property values. You can customize this preference by annotating entire classes or individual properties with `@AccessType`:
====
[source,java]
----
@AccessType(PROPERTY)
class Person { <1>
private String id;
@AccessType(FIELD) <2>
private String name;
public void setId(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
// other getters/setters omitted for brevity.
}
----
<1> Annotating a class with `@AccessType(PROPERTY)` uses property accessors (getters and setters) to retrieve and update properties.
<2> You can annotate individual properties with `@AccessType` to switch to field or property access.
Spring Data inspects property accessor methods and the field to find an annotation.
====
Spring Data can use reflection and generated bytecode to access properties.
Bytecode is generated on the fly and does not require any upfront or runtime instrumentation.
Generated bytecode access is about 5% to 7% faster than reflection access, but it imposes certain limits:
* Types must not reside in the default or under the `java` package.
* The used Java Runtime must allow for declaring classes in the originating `ClassLoader`. Java 9 and newer impose certain limitations.
== Kotlin support
By default, Spring Data attempts to use generated property accessors and falls back to reflection-based access if a limitation is detected.
Spring Data adapts specifics of Kotlin to allow object creation and mutation.
[[mapping.mutability.constructorcreation]]
== Constructor Creation
When reading an entity from the data store, Spring Data's `EntityReader` is able to create objects by invoking its persistence constructor and to pass arguments to populate property values.
Consider the following `Person` class:
====
[source,java]
----
class Person {
private @Id String id;
private String name;
Person(String id, String name) {
this.id = id;
this.name = name;
}
// other methods omitted for brevity.
}
----
====
This entity can be constructed entirely from a constructor call by passing `id` and `name` parameters.
The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceConstructor` annotation. The values to be used for the constructor parameters are resolved in the following way:
* If the Java type has a property whose name matches the given field of the input document, then it's property information is used to select the appropriate constructor parameter to pass the input field value to. This works only if the parameter name information is present in the java `.class` files which can be achieved by compiling the source with debug information or using the new `-parameters` command-line switch for `javac` in Java 8.
* Otherwise, a `MappingException` is thrown to indicate that the given constructor parameter could not be bound.
Let's take our `Person` class and add another constructor taking just the `id` parameter:
====
[source,java]
----
class Person {
private @Id String id;
private String name;
Person(String id) {
this.id = id;
this.name = "unknown";
}
@PersistenceConstructor
Person(String id, String name) {
this.id = id;
this.name = name;
}
// other methods omitted for brevity.
}
----
====
This class has two constructors of which one is annotated with `@PersistenceConstructor`. Spring Data will solely use this constructor to create instances of `Person`.
=== Kotlin classes
=== 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. Consider the following `data` class `Person`:
@ -467,13 +257,15 @@ data class Person(var id: String, val name: String = "unknown")
@@ -467,13 +257,15 @@ data class Person(var id: String, val name: String = "unknown")
Every time the `name` parameter is either not part of the result or its value is `null`, then the `name` defaults to `unknown`.
Spring Data can use reflection and generated bytecode to create object instances.
Bytecode is generated on the fly and does not require any upfront or runtime instrumentation.
Generated bytecode creation is about 25% faster than reflection access but it imposes certain limits:
=== Property population of Kotlin data classes
* Types must not reside in the default or under the `java` package.
* Types and their constructors must be `public`
* Types that are inner classes must be `static`.
* The used Java Runtime must allow for declaring classes in the originating `ClassLoader`. Java 9 and newer impose certain limitations.
In Kotlin, all classes are immutable by default and require explicit property declarations to define mutable properties. Consider the following `data` class `Person`:
By default, Spring Data attempts to use generated entity instantiatiors and falls back to reflection-based ones if a limitation is detected.
====
[source,java]
----
data class Person(val id: String, val name: String)
----
====
This class is effectively immutable. It allows to create 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.