Object Mapping Fundamentals
本节涵盖了 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不可变性的基础知识。请注意,此部分只适用于未使用基础数据存储的对象映射(如 JPA)的 Spring Data 模块。还要务必查阅特定于存储的部分,了解特定于存储的对象映射,如索引、自定义列或字段名称等。 Spring Data 对象映射的核心职责是创建域对象实例并将存储本地数据结构映射到这些实例上。这意味着我们需要两个基本步骤:
-
通过使用公开的其中一个构造函数创建实例。
-
实例填充以实现所有公开的属性。
Object creation
Spring Data 自动尝试检测一个持久实体的构造函数,以便用于实现该类型的对象。解决算法的工作方式如下:
-
如果有使用
@PersistenceCreator
注释的单个静态工厂方法,则使用该方法。 -
如果有单个构造函数,则使用该构造函数。
-
如果有多个构造函数,并且只有一个使用
@PersistenceCreator
注释,则使用该构造函数。 -
如果该类型是一个 Java
Record
,则使用规范构造函数。 -
如果有无参数构造函数,则会使用它。将忽略其他构造函数。
值解析假定构造函数/工厂方法的参数名称与实体的属性名称匹配,即解析将执行得好像要填充该属性一样,包括映射中的所有定制(不同的数据存储列或字段名称等)。这也要求在类文件或构造函数上有 @ConstructorProperties
注释中提供参数名称信息。
可以通过使用 Spring Framework 的 @Value
值注释来使用特定于存储的 SpEL 表达式来定制值解析。请查阅有关具体存储的映射的部分以获取更多详细信息。
为了避免反射时间开销,Spring Data 对象创建在默认情况下使用在运行时生成的工厂类,该类将直接调用领域类的构造函数。对于以下示例类型:
class Person {
Person(String firstname, String lastname) { … }
}
我们将在运行时为这个类创建一个语义上等价的工厂类:
class PersonObjectInstantiator implements ObjectInstantiator {
Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}
这给了我们超过反射 10% 的迂回式性能提升。为了使领域类适用于这种优化,它需要遵守一组约束:
-
它不得是私有类
-
它不得是非静态内部类
-
它不得是 CGLib 代理类
-
Spring Data 使用的构造函数不得为私有
如果符合上述任何一个标准,Spring Data 将回退到通过反射进行实体实例化。
Property population
在创建了该实体的一个实例之后,Spring Data 填充该类的所有剩余的持久属性。除非已通过实体的构造函数填充(即通过其构造函数参数列表消耗),否则将首先填充标识符属性,以允许解析循环对象引用。在此之后,所有尚未被构造函数填充的非瞬态属性都会在实体实例上设置。为此,我们使用以下算法:
-
如果属性为不可变但公开
with…
方法(如下所示),我们将使用with…
方法创建具有新属性值的新实体实例。 -
如果定义了属性访问(即通过 getter 和 setter 访问),我们将调用 setter 方法。
-
如果属性为可变的,我们将直接设置字段。
-
如果属性不可变,我们将使用持久化操作中使用的构造函数(见 Object creation)创建实例的副本。
-
默认情况下,将直接设置字段值。
类似于我们在对象构造中的优化,映射.object-creation.details,我们还使用 Spring Data 运行时生成的访问器类与实体实例进行交互。
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.lastame);
}
void setLastname(String lastname) {
this.lastname = lastname;
}
}
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
}
}
}
1 | PropertyAccessor 持有底层对象的不可变实例。这是为了允许对其他不可变属性进行突变。 |
2 | 默认情况下,Spring Data 使用字段访问来读写属性值。根据 private 域的可见性规则,MethodHandles 用于与域进行交互。 |
3 | 类公开一个 withId(…) 方法,该方法用于设置标识符,例如,当将一个实例插入到数据存储中并且生成了一个标识符时。调用 withId(…) 创建一个新的 Person 对象。所有后续的突变将在新实例中进行,而不会影响前面的实例。 |
4 | 使用属性访问允许直接调用方法,而不使用 MethodHandles 。 |
这给了我们超过反射 25% 的迂回式性能提升。为了使领域类适用于这种优化,它需要遵守一组约束:
-
类型不得驻留在默认值或
java
包之下。 -
类型及其构造函数必须为
public
-
类型为内部类的类型必须为
static
。 -
使用的 Java 运行时必须允许在原始
ClassLoader
中声明类。Java 9 及更新版本施加了某些限制。
在默认情况下,Spring Data 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的属性访问器。
让我们看看以下实体:
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
static Person of(String firstname, String lastname, LocalDate birthday) { 6
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { 6
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;
}
}
1 | 标识符属性为最终值,但在构造函数中将其设置为 null 。类公开一个 withId(…) 方法,该方法用于设置标识符,例如,当将一个实例插入到数据存储中并且生成了一个标识符时。原始 Person 实例保持不变,因为创建了一个新的实例。通常将相同的模式应用于其他由存储管理但可能必须更改为持久化操作的属性。wither 方法是可选的,因为持久化构造函数(见 6)实际上是一个复制构造函数,并且设置属性将转换为创建一个应用了新标识符值的新实例。 |
2 | firstname 和 lastname 属性是可通过 Getter 公开的简单不可变属性。 |
3 | age 属性是不可变但从 birthday 属性派生的属性。采用所示设计,数据库值将胜过默认值,这是因为 Spring Data 仅使用声明的构造函数。即使目的是计算优先,但此构造函数也必须将 age 作为参数(以潜在忽略它),否则属性填充步骤将尝试设置年龄字段并因为该字段不可变且没有存在 with… 方法而失败。 |
4 | comment 属性是可变的,通过直接设置其字段进行填充。 |
5 | remarks 属性是可变的,通过调用赋值器方法进行填充。 |
6 | 类公开了一个工厂方法和一个用于创建对象的构造函数。此处的核心概念是使用工厂方法,而不是附加构造函数,以避免必须通过 @PersistenceCreator 消除构造函数二义性。在工厂方法中处理属性的默认值。如果您想让 Spring Data 使用工厂方法实例化对象,请使用 @PersistenceCreator 为其添加注释。 |
General recommendations
-
Try to stick to immutable objects — 不可变对象易于创建,因为具体化一个对象就是调用其构造函数。此外,这避免在您的领域对象中布满通过设置器方法允许客户端代码处理对象状态的情况。如果您需要这些代码,请使用程序包保护它们,以便只能通过有限的同置类型调用它们。仅构造函数具体化比属性填充快 30%。
-
Provide an all-args constructor — 即使您不能或不想将实体建模为不可变值,提供一个构造函数(它将所有实体属性作为参数,包括可变属性)仍然有价值,因为这允许对象映射跳过属性填充,以获得最佳性能。
-
Use factory methods instead of overloaded constructors to avoid `@PersistenceCreator` — 为了获得最佳性能,通常需要一个所有参数构造函数,我们通常希望公开更多应用用例特定的构造函数,这些构造函数省略自动生成标识符等信息。一个成熟的模式是使用静态工厂方法公开所有参数构造函数的这些变体。
-
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 an all-arguments persistence constructor (preferred) or a
with…
method — -
Use Lombok to avoid boilerplate code — 由于持久化操作通常需要一个获取所有参数的构造函数,因此,它们的声明会变成枯燥地重复参数到字段赋值的样板,而使用 Lombok 的
@AllArgsConstructor
可以很好地避免这种情况。
Overriding Properties
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);
}
}
这两个类都使用可分配的类型定义了 field
。但是,SubType
隐藏了 SuperType.field
。根据类设计,使用构造函数可能是设置 SuperType.field
的唯一默认方式。或者,在设置器中调用 super.setField(…)
可以设置 SuperType
中的 field
。由于这些属性共享相同名称,但可能表示两个不同的值,所有这些机制在某种程度上都会产生冲突。如果类型不可分配,Spring Data 将跳过超类型属性。也就是说,被重写的属性的类型必须可以分配给其超类型属性类型才能被注册为覆盖项,否则超类型属性被视为瞬态。我们通常建议使用不同的属性名称。
Spring Data 模块普遍支持包含不同值的重写属性。从编程模型的角度来看,有几件事需要考虑:
-
哪种属性应该持久化(默认为所有已声明的属性)?您可以添加
@Transient
注释到属性上以排除它们。 -
如何在数据存储中表示属性?不同值使用相同的字段/列名称通常会导致数据损坏,因此您应该至少使用一个显式字段/列名称为属性添加注释。
-
由于不能在不针对赋值器实现做出进一步假设的情况下设置超属性,所以不能使用
@AccessType(PROPERTY)
。
Kotlin support
Spring Data 适应 Kotlin 的特殊性,允许创建和更改对象。
Kotlin object creation
支持实例化 Kotlin 类,所有类在默认情况下都是不可变的,需要显式属性声明才能定义可变属性。
Spring Data 自动尝试检测一个持久实体的构造函数,以便用于实现该类型的对象。解决算法的工作方式如下:
-
如果有一个带
@PersistenceCreator
注释的构造函数,则使用它。 -
如果类型是 Kotlin data cass,则使用主构造函数。
-
如果有使用
@PersistenceCreator
注释的单个静态工厂方法,则使用该方法。 -
如果有单个构造函数,则使用该构造函数。
-
如果有多个构造函数,并且只有一个使用
@PersistenceCreator
注释,则使用该构造函数。 -
如果该类型是一个 Java
Record
,则使用规范构造函数。 -
如果有无参数构造函数,则会使用它。将忽略其他构造函数。
考虑以下 data
类 Person
:
data class Person(val id: String, val name: String)
上述类编译为具有显式构造函数的典型类。我们可以通过添加另一个构造函数并用 @PersistenceCreator
注释它来定制此类,以指示构造函数偏好:
data class Person(var id: String, val name: String) {
@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}
Kotlin 支持参数可选择性,允许在不提供参数时使用默认值。当 Spring Data 检测到带有参数默认值的构造函数时,它会让这些参数保持缺失状态(或仅返回 null
),以便 Kotlin 可以应用参数默认值。考虑以下对 name
应用参数默认值的类
data class Person(var id: String, val name: String = "unknown")
每次当 name
参数既不属于结果的一部分或其值为 null
,则 name
将默认为 unknown
。
Property population of Kotlin data classes
在 Kotlin 中,所有类默认情况下是不可变的,需要显式声明属性来定义可变属性。考虑下面的 data
类 Person
:
data class Person(val id: String, val name: String)
这个类实际上是不可变的。它允许创建新的实例,因为 Kotlin 生成了一个 copy(…)
方法,该方法创建新的对象实例,复制所有属性值(从现有对象)并将作为参数提供给该方法的属性值应用进来。
Kotlin Overriding Properties
Kotlin 允许声明 `` 来更改子类中的属性。
open class SuperType(open var field: Int)
class SubType(override var field: Int = 1) :
SuperType(field) {
}
这种安排使两个名称为 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;
}
}
SubType
上的 getter 和 setter 仅设置 SubType.field
而不设置 SuperType.field
。在这样的安排中,使用构造函数是设置 SuperType.field
的唯一默认方法。向 SubType
中添加一个方法以通过 this.SuperType.field = …
来设置 SuperType.field
是可能的,但是不属于受支持的约定。属性覆盖在某种程度上创建了冲突,因为这些属性共享相同名称,但可能表示两个不同的值。我们通常建议使用不同的属性名称。
Spring Data 模块普遍支持包含不同值的重写属性。从编程模型的角度来看,有几件事需要考虑:
-
哪种属性应该持久化(默认为所有已声明的属性)?您可以添加
@Transient
注释到属性上以排除它们。 -
如何在数据存储中表示属性?不同值使用相同的字段/列名称通常会导致数据损坏,因此您应该至少使用一个显式字段/列名称为属性添加注释。
-
由于不能设置超属性,所以不能使用
@AccessType(PROPERTY)
。
Kotlin Value Classes
Kotlin 值类被设计用于更具表现力的领域模型来使基础概念明确化。Spring Data 可以读取和写入使用值类定义属性的类型。
考虑以下领域模型:
@JvmInline
value class EmailAddress(val theAddress: String) 1
data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) 2
1 | 具有非空值类型的简单值类。 |
2 | 数据类使用 EmailAddress 值类定义属性。 |
在已编译类中,使用非基本值类型而非空属性会展平为该值类型。可空基本值类型或可空值中值类型将用包装器类型表示,并影响在数据库中如何表示值类型。 |