跳到主要内容

对象映射基础

Doubao 中英对照 Object Mapping Fundamentals

本节介绍 Spring Data 对象映射、对象创建、字段与属性访问、可变性与不可变性的基础知识。请注意,本节仅适用于不使用底层数据存储对象映射(如 JPA)的 Spring Data 模块。另请务必参阅特定存储部分,了解特定存储的对象映射,例如索引、自定义列名或字段名等。

Spring Data 对象映射的核心职责是创建领域对象的实例,并将存储原生的数据结构映射到这些实例上。这意味着我们需要两个基本步骤:

  1. 通过使用所暴露的构造函数之一来创建实例。

  2. 填充实例,以物化所有暴露的属性。

对象创建​

Spring Data 会自动尝试检测持久化实体的构造函数,用于物化该类型的对象。解析算法的工作方式如下:

  1. 如果只有一个标注了 @PersistenceCreator 的静态工厂方法,则使用它。

  2. 如果只有一个构造函数,则使用它。

  3. 如果有多个构造函数,且恰好有一个标注了 @PersistenceCreator,则使用它。

  4. 如果该类型是 Java Record,则使用规范构造函数。

  5. 如果存在无参构造函数,则使用它。其他构造函数将被忽略。

值解析假定构造函数/工厂方法的参数名与实体的属性名相匹配,也就是说,解析将像要填充该属性一样执行,包括映射中的所有自定义设置(不同的数据存储列名或字段名等)。这还要求类文件中包含可用的参数名信息,或者构造函数上存在 @ConstructorProperties 注解。

可以使用 Spring Framework 的 @Value 值注解并配合特定存储的 SpEL 表达式来自定义值解析。有关更多详细信息,请参阅特定存储映射一节。

Object creation internals

To avoid the overhead of reflection, Spring Data object creation uses a factory class generated at runtime by default, which will call the domain classes constructor directly. I.e. for this example type:

class Person {
Person(String firstname, String lastname) { … }
}
java

we will create a factory class semantically equivalent to this one at runtime:

class PersonObjectInstantiator implements ObjectInstantiator {

Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}
java

This gives us a roundabout 10% performance boost over reflection. For the domain class to be eligible for such optimization, it needs to adhere to a set of constraints:

  • it must not be a private class

  • it must not be a non-static inner class

  • it must not be a CGLib proxy class

  • the constructor to be used by Spring Data must not be private

If any of these criteria match, Spring Data will fall back to entity instantiation via reflection.

属性填充​

创建实体的实例后,Spring Data 会填充该类所有剩余的持久化属性。除非已由实体的构造函数填充(即通过其构造函数参数列表消费),否则会先填充标识符属性,以支持解析循环对象引用。此后,将所有尚未由构造函数填充的非瞬态属性设置到实体实例上。为此我们使用以下算法:

  1. 如果属性是不可变的,但暴露了 with… 方法(见下文),我们使用 with… 方法创建一个带有新属性值的新实体实例。

  2. 如果定义了属性访问(即通过 getter 和 setter 访问),我们调用 setter 方法。

  3. 如果属性是可变的,我们直接设置字段。

  4. 如果属性是不可变的,我们使用持久化操作所用的构造函数(请参阅 对象创建)来创建实例的副本。

  5. 默认情况下,我们直接设置字段值。

Property population internals

Similarly to our optimizations in object construction we also use Spring Data runtime generated accessor classes to interact with the entity instance.

class Person {

private final Long id;
private String firstname;
private @AccessType(Type.PROPERTY) String lastname;

Person() {
this.id = null;
}

Person(Long id, String firstname, String lastname) {
// Field assignments
}

Person withId(Long id) {
return new Person(id, this.firstname, this.lastname);
}

void setLastname(String lastname) {
this.lastname = lastname;
}
}
java
class PersonPropertyAccessor implements PersistentPropertyAccessor {

private static final MethodHandle firstname; 2

private Person person; 1

public void setProperty(PersistentProperty property, Object value) {

String name = property.getName();

if ("firstname".equals(name)) {
firstname.invoke(person, (String) value); 2
} else if ("id".equals(name)) {
this.person = person.withId((Long) value); 3
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value); 4
}
}
}
java
  • PropertyAccessor’s hold a mutable instance of the underlying object. This is, to enable mutations of otherwise immutable properties.

  • 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.

  • 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.

  • 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.

我们来看以下实体:

class Person {

private final @Id Long id; 1
private final String firstname, lastname; 2
private final LocalDate birthday;
private final int age; 3

private String comment; 4
private @AccessType(Type.PROPERTY) String remarks; 5
private @Transient String summary; 6

static Person of(String firstname, String lastname, LocalDate birthday) { 7

return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}

Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { 7

this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}

Person withId(Long id) { 1
return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
}

void setRemarks(String remarks) { 5
this.remarks = remarks;
}
}
java
  • 标识符属性是 final 的,但在构造函数中被设置为 null。该类暴露了一个 withId(…) 方法,用于设置标识符,例如将实例插入数据存储并已生成标识符时。创建新实例后,原始 Person 实例保持不变。对于其他由存储管理、但在持久化操作中可能需要更改的属性,通常也会应用相同的模式。wither 方法是可选的,因为持久化构造函数(见 6)实际上是一个复制构造函数,设置该属性会被转换为创建一个应用了新标识符值的全新实例。

  • firstname 和 lastname 属性是普通的不可变属性,可能通过 getter 暴露。

  • age 属性是不可变的,但派生自 birthday 属性。在所示设计中,由于 Spring Data 使用唯一声明的构造函数,数据库值将优先于默认值。即使意图是优先使用计算结果,重要的是该构造函数也应将 age 作为参数(以便可能忽略它),否则属性填充步骤会尝试设置 age 字段,并因它是不可变的且不存在 with… 方法而失败。

  • comment 属性是可变的,通过直接设置其字段来填充。

  • remarks 属性是可变的,通过调用 setter 方法来填充。

  • summary 属性是瞬态的,不会被持久化,因为它标注了 @Transient。Spring Data 不使用 Java 的 transient 关键字来将属性排除在持久化之外,因为 transient 是 Java 序列化框架的一部分。请注意,该属性可以与持久化构造函数一起使用,但其值默认为 null(如果属性类型是基本类型,则为相应的基本类型初始值)。

  • 该类暴露了一个工厂方法和一个构造函数用于对象创建。这里的核心思想是使用工厂方法而非额外的构造函数,以避免通过 @PersistenceCreator 进行构造函数消歧。相反,属性的默认值在工厂方法内部处理。如果您希望 Spring Data 使用工厂方法进行对象实例化,请用 @PersistenceCreator 标注它。

一般建议​

  • 尽量坚持使用不可变对象 — 不可变对象易于创建,因为物化对象只需调用其构造函数。此外,这还避免了在领域对象中到处散布 setter 方法(这些方法会允许客户端代码操纵对象状态)。如果您确实需要这些 setter,最好将它们设为包级可见,以便只有有限的同包类型能调用它们。仅通过构造函数物化比属性填充快多达 30%。

  • 提供一个全参数构造函数 — 即使您不能或不想将实体建模为不可变值,提供一个接收实体所有属性(包括可变属性)作为参数的构造函数仍然有价值,因为这可以让对象映射跳过属性填充,以获得最佳性能。

  • 使用工厂方法而非重载构造函数,以避免 @PersistenceCreator — 既然为获得最佳性能需要一个全参数构造函数,我们通常还希望暴露更多针对应用用例的构造函数,它们会省略自动生成的标识符等内容。一个成熟的模式是改用静态工厂方法来暴露这些全参数构造函数的变体。

  • 确保您遵守那些允许使用生成的实例化器和属性访问器类的约束 —

  • 对于要生成的标识符,仍应使用 final 字段,并配合全参数持久化构造函数(首选)或 with… 方法 —

  • 使用 Lombok 避免样板代码 — 由于持久化操作通常需要一个接收所有参数的构造函数,其声明会变成对参数到字段赋值这一样板代码的繁琐重复,而使用 Lombok 的 @AllArgsConstructor 可以最好地避免这一点。

重写属性​

Java 允许灵活设计领域类,子类可以定义一个在其父类中已以相同名称声明过的属性。请看以下示例:

public class SuperType {

private CharSequence field;

public SuperType(CharSequence field) {
this.field = field;
}

public CharSequence getField() {
return this.field;
}

public void setField(CharSequence field) {
this.field = field;
}
}

public class SubType extends SuperType {

private String field;

public SubType(String field) {
super(field);
this.field = field;
}

@Override
public String getField() {
return this.field;
}

public void setField(String field) {
this.field = field;

// optional
super.setField(field);
}
}
java

两个类都使用可赋值的类型定义了一个 field。然而 SubType 遮蔽了 SuperType.field。根据类设计的不同,使用构造函数可能是设置 SuperType.field 的唯一默认方式。或者,在 setter 中调用 super.setField(…) 可以设置 SuperType 中的 field。所有这些机制在某种程度上都会产生冲突,因为这些属性共享相同的名称,却可能代表两个不同的值。如果类型不可赋值,Spring Data 会跳过父类型属性。也就是说,被重写属性的类型必须可赋值到其父类型属性的类型,才会被注册为重写,否则该父类型属性被视为瞬态。我们一般建议使用不同的属性名。

Spring Data 模块通常支持持有不同值的被重写属性。从编程模型的角度来看,有几点需要考虑:

  1. 应该持久化哪个属性(默认为所有声明的属性)?您可以通过用 @Transient 标注属性来将其排除。

  2. 如何在您的数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,因此您应该至少用显式的字段/列名标注其中一个属性。

  3. 不能使用 @AccessType(PROPERTY),因为在不对 setter 实现做进一步假设的情况下,通常无法设置父属性。

Kotlin 支持​

Spring Data 适配了 Kotlin 的一些特性,以支持对象创建和修改。

Kotlin 对象创建​

Kotlin 类支持被实例化,所有类默认都是不可变的,需要显式声明属性才能定义可变属性。

Spring Data 会自动尝试检测持久化实体的构造函数,用于物化该类型的对象。解析算法的工作方式如下:

  1. 如果存在标注了 @PersistenceCreator 的构造函数,则使用它。

  2. 如果该类型是 Kotlin 数据类,则使用主构造函数。

  3. 如果只有一个标注了 @PersistenceCreator 的静态工厂方法,则使用它。

  4. 如果只有一个构造函数,则使用它。

  5. 如果有多个构造函数,且恰好有一个标注了 @PersistenceCreator,则使用它。

  6. 如果该类型是 Java Record,则使用规范构造函数。

  7. 如果存在无参构造函数,则使用它。其他构造函数将被忽略。

请看以下 data 类 Person:

data class Person(val id: String, val name: String)
kotlin

上面的类会编译成一个带有显式构造函数的典型类。我们可以通过添加另一个构造函数并用 @PersistenceCreator 标注它来自定义该类,以表明构造函数偏好:

data class Person(var id: String, val name: String) {

@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}
kotlin

Kotlin 通过允许在未提供参数时使用默认值来支持参数可选。当 Spring Data 检测到带有参数默认值的构造函数时,如果数据存储未提供值(或仅返回 null),它会让这些参数缺省,以便 Kotlin 应用参数默认值。请看以下为 name 应用参数默认值的类:

data class Person(var id: String, val name: String = "unknown")
kotlin

每当 name 参数不在结果中或其值为 null 时,name 默认为 unknown。

备注

Spring Data 不支持委托属性。映射元数据会为 Kotlin 数据类过滤掉委托属性。在所有其他情况下,您可以通过用 @Transient 标注该属性,来排除委托属性的合成字段。

Kotlin 数据类的属性填充​

在 Kotlin 中,所有类默认都是不可变的,需要显式声明属性才能定义可变属性。请看以下 data 类 Person:

data class Person(val id: String, val name: String)
kotlin

这个类实际上是不可变的。它允许创建新实例,因为 Kotlin 会生成一个 copy(…) 方法,该方法创建新的对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用到方法上。

Kotlin 重写属性​

Kotlin 允许声明属性重写来修改子类中的属性。

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
SuperType(field) {
}
kotlin

这样的安排会产生两个名为 field 的属性。Kotlin 会为每个类中的每个属性生成属性访问器(getter 和 setter)。实际上,代码如下所示:

public class SuperType {

private int field;

public SuperType(int field) {
this.field = field;
}

public int getField() {
return this.field;
}

public void setField(int field) {
this.field = field;
}
}

public final class SubType extends SuperType {

private int field;

public SubType(int field) {
super(field);
this.field = field;
}

public int getField() {
return this.field;
}

public void setField(int field) {
this.field = field;
}
}
java

SubType 上的 getter 和 setter 只设置 SubType.field,而不设置 SuperType.field。在这样的安排下,使用构造函数是设置 SuperType.field 的唯一默认方式。在 SubType 中添加一个方法,通过 this.SuperType.field = … 来设置 SuperType.field 是可行的,但这不在受支持的约定之列。属性重写在某种程度上会产生冲突,因为这些属性共享相同的名称,却可能代表两个不同的值。我们一般建议使用不同的属性名。

Spring Data 模块通常支持持有不同值的被重写属性。从编程模型的角度来看,有几点需要考虑:

  1. 应该持久化哪个属性(默认为所有声明的属性)?您可以通过用 @Transient 标注属性来将其排除。

  2. 如何在您的数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,因此您应该至少用显式的字段/列名标注其中一个属性。

  3. 不能使用 @AccessType(PROPERTY),因为父属性无法设置。

Kotlin 值类​

Kotlin 值类旨在构建更具表达力的领域模型,使底层概念更加明确。Spring Data 可以读写使用值类定义属性的类型。

请看以下领域模型:

@JvmInline
value class EmailAddress(val theAddress: String) 1

data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) 2
kotlin
  • 一个带有非空值类型的简单值类。

  • 使用 EmailAddress 值类定义属性的数据类。

备注

使用非基本类型值类型的非空属性,在编译后的类中会被扁平化到该值类型。可空的基本类型值类型或可空的值中值类型会以其包装类型表示,这会影响值类型在数据库中的表示方式。