原标题:Spring认证中国教育管理中心-Apache Geode 的 Spring 数据教程二十一(Spring中国教育管理中心)
9.1.对象映射基础
本节涵盖 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不变性的基础知识。请注意,本节仅适用于不使用底层数据存储(如 JPA)的对象映射的 Spring Data 模块。此外,请务必查阅特定于存储的部分以获取特定于存储的对象映射,例如索引、自定义列或字段名称等。
Spring Data 对象映射的核心职责是创建域对象的实例并将存储本机数据结构映射到这些实例上。这意味着我们需要两个基本步骤:
- 使用公开的构造函数之一创建实例。
- 实例填充以实现所有公开的属性。
9.1.1.对象创建
Spring Data 会自动尝试检测要用于具体化该类型对象的持久实体的构造函数。解析算法的工作原理如下:
- 如果只有一个构造函数,则使用它。
- 如果有多个构造函数并且只有一个用 注释@PersistenceConstructor,则使用它。
- 如果存在无参数构造函数,则使用它。其他构造函数将被忽略。
值解析假定构造函数参数名称与实体的属性名称匹配,即解析将被执行,就像要填充属性一样,包括映射中的所有自定义(不同的数据存储列或字段名称等)。这还需要类文件中可用的参数名称信息或@ConstructorProperties构造函数中存在的注释。
可以通过使用@Value特定于商店的 SpEL 表达式使用 Spring Framework 的值注释来自定义值解析。有关更多详细信息,请参阅有关商店特定映射的部分。
对象创建内部
为了避免反射的开销,Spring Data 对象创建默认使用运行时生成的工厂类,它会直接调用域类构造函数。即对于此示例类型:
代码语言:javascript复制class Person {
Person(String firstname, String lastname) { … }
}
我们将在运行时创建一个语义上等同于这个的工厂类:
代码语言:javascript复制class PersonObjectInstantiator implements ObjectInstantiator {
Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}
这使我们比反射提高了大约 10% 的性能。要使域类有资格进行此类优化,它需要遵守一组约束:
- 它不能是私人课程
- 它不能是非静态内部类
- 它不能是 CGLib 代理类
- Spring Data 使用的构造函数不能是私有的
如果这些条件中的任何一个匹配,Spring Data 将通过反射回退到实体实例化。
9.1.2.物业人口
一旦创建了实体的实例,Spring Data 就会填充该类的所有剩余持久属性。除非实体的构造函数已经填充(即通过其构造函数参数列表消耗),标识符属性将首先填充以允许循环对象引用的解析。之后,所有尚未由构造函数填充的非瞬态属性都在实体实例上设置。为此,我们使用以下算法:
- 如果属性是不可变的但公开了一个with…方法(见下文),我们使用该with…方法创建一个具有新属性值的新实体实例。
- 如果定义了属性访问(即通过 getter 和 setter 访问),我们将调用 setter 方法。
- 如果属性是可变的,我们直接设置字段。
- 如果属性是不可变的,我们将使用持久性操作(请参阅对象创建)使用的构造函数来创建实例的副本。
- 默认情况下,我们直接设置字段值。
财产人口内部结构
与我们在对象构造中的优化类似,我们也使用 Spring Data 运行时生成的访问器类与实体实例进行交互。
代码语言:javascript复制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;
}
}
示例 1. 生成的属性访问器
代码语言:javascript复制class PersonPropertyAccessor implements PersistentPropertyAccessor {
private static final MethodHandle firstname;
private Person person;
public void setProperty(PersistentProperty property, Object value) {
String name = property.getName();
if ("firstname".equals(name)) {
firstname.invoke(person, (String) value);
} else if ("id".equals(name)) {
this.person = person.withId((Long) value);
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value);
}
}
}
PropertyAccessor 持有底层对象的可变实例。这是为了启用其他不可变属性的突变。
默认情况下,Spring Data 使用字段访问来读取和写入属性值。根据private字段的可见性规则,MethodHandles用于与字段进行交互。
该类公开了一个withId(…)用于设置标识符的方法,例如,当一个实例插入到数据存储中并生成一个标识符时。调用withId(…)创建一个新Person对象。所有后续的突变都将在新实例中发生,而前一个则保持不变。
使用属性访问允许直接方法调用而不使用MethodHandles.
这使我们比反射提高了大约 25% 的性能。要使域类有资格进行此类优化,它需要遵守一组约束:
类型不得位于默认值或java包下。
类型及其构造函数必须是 public
属于内部类的类型必须是static.
使用的 Java 运行时必须允许在原始ClassLoader. Java 9 和更新版本施加了某些限制。
默认情况下,Spring Data 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的访问器。
让我们看一下以下实体:
示例 2. 示例实体
代码语言:javascript复制class Person {
private final @Id Long id;
private final String firstname, lastname;
private final LocalDate birthday;
private final int age;
private String comment;
private @AccessType(Type.PROPERTY) String remarks;
static Person of(String firstname, String lastname, LocalDate birthday) {
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) {
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}
Person withId(Long id) {
return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
}
void setRemarks(String remarks) {
this.remarks = remarks;
}
}
identifier 属性是最终的,但null在构造函数中设置为。该类公开了一个withId(…)用于设置标识符的方法,例如,当一个实例插入到数据存储中并生成一个标识符时。Person创建新实例时,原始实例保持不变。相同的模式通常应用于由存储管理但可能必须为持久性操作更改的其他属性。wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,并且设置该属性将被转换为创建一个应用新标识符值的新实例。
的firstname和lastname特性是通过吸气剂可能暴露普通不可变属性。
该age属性是不可变的,但源自该birthday属性。使用所示设计,数据库值将胜过默认值,因为 Spring Data 使用唯一声明的构造函数。即使意图是计算应该是首选,重要的是此构造函数也将age作为参数(可能会忽略它),否则属性填充步骤将尝试设置年龄字段并由于它不可变且没有with…方法而失败在场。
该comment属性是可变的,通过直接设置其字段来填充。
的remarks特性是可变的,并且通过设置填充comment直接字段或通过调用用于setter方法
该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是额外的构造函数,以避免需要通过@PersistenceConstructor. 相反,属性的默认设置是在工厂方法中处理的。
9.1.3.一般建议
- 尽量坚持不可变对象 ——不可变对象很容易创建,因为具体化一个对象只是调用它的构造函数的问题。此外,这避免了您的域对象中充斥着允许客户端代码操作对象状态的 setter 方法。如果您需要这些,最好将它们打包保护,以便它们只能由有限数量的并置类型调用。仅构造函数实现比属性填充快 30%。
- 提供一个全参数构造函数 ——即使你不能或不想将你的实体建模为不可变值,提供一个将实体的所有属性作为参数的构造函数仍然有价值,包括可变的,因为这允许对象映射以跳过属性填充以获得最佳性能。
- 使用工厂方法而不是重载构造函数来避免@PersistenceConstructor ——为了获得最佳性能需要一个全参数构造函数,我们通常希望公开更多应用程序用例特定的构造函数,这些构造函数省略自动生成的标识符等。这是一种既定的模式,而不是使用静态工厂方法来公开 all-args 构造函数的这些变体。
- 确保遵守允许使用生成的实例化器和属性访问器类的约束 ——
- 对于要生成的标识符,仍然使用 final 字段与全参数持久性构造函数(首选)或with…方法相结合 ——
- 使用 Lombok 避免样板代码 ——由于持久性操作通常需要一个接受所有参数的构造函数,因此它们的声明变成了对字段分配的样板参数的乏味重复,而使用 Lombok 的@AllArgsConstructor.
覆盖属性
Java 允许灵活设计领域类,其中子类可以定义已在其超类中使用相同名称声明的属性。考虑以下示例:
代码语言:javascript复制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);
}
}
这两个类都定义了一个fieldusing 可分配类型。SubType然而阴影SuperType.field。根据类设计,使用构造函数可能是设置 的唯一默认方法SuperType.field。或者,调用super.setField(…)setter 可以设置fieldin SuperType。所有这些机制都会在某种程度上产生冲突,因为属性共享相同的名称,但可能代表两个不同的值。如果类型不可分配,Spring Data 会跳过超类型属性。也就是说,被覆盖属性的类型必须可以分配给它的超类型属性类型才能注册为覆盖,否则超类型属性被认为是瞬态的。我们通常建议使用不同的属性名称。
Spring Data 模块通常支持包含不同值的覆盖属性。从编程模型的角度来看,需要考虑以下几点:
- 应该保留哪个属性(默认为所有声明的属性)?您可以通过使用 注释这些属性来排除属性@Transient。
- 如何表示数据存储中的属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此您应该使用明确的字段/列名称来注释至少一个属性。
- Using@AccessType(PROPERTY)不能被使用,因为如果不对 setter 实现做任何进一步的假设,通常不能设置超级属性。
9.1.4.Kotlin 支持
Spring Data 调整了 Kotlin 的细节以允许创建和更改对象。
Kotlin 对象创建
Kotlin 类支持实例化,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。考虑以下data类Person:
代码语言:javascript复制data class Person(val id: String, val name: String)
上面的类编译为具有显式构造函数的典型类。我们可以通过添加另一个构造函数来自定义这个类,并使用注释@PersistenceConstructor来指示构造函数首选项:
代码语言:javascript复制data class Person(var id: String, val name: String) {
@PersistenceConstructor
constructor(id: String) : this(id, "unknown")
}
Kotlin 通过允许在未提供参数时使用默认值来支持参数可选性。当 Spring Data 检测到具有参数默认值的构造函数时,如果数据存储不提供值(或简单地返回null),它就会使这些参数不存在,因此 Kotlin 可以应用参数默认值。考虑以下应用参数默认值的类name
代码语言:javascript复制data class Person(var id: String, val name: String = "unknown")
每次name参数不是结果的一部分或其值为 时null,则name默认为unknown。
Kotlin 数据类的属性填充
在 Kotlin 中,默认情况下所有类都是不可变的,并且需要明确的属性声明来定义可变属性。考虑以下data类Person:
代码语言:javascript复制data class Person(val id: String, val name: String)
这个类实际上是不可变的。它允许创建新实例,因为 Kotlin 生成copy(…)创建新对象实例的方法,该方法从现有对象复制所有属性值并将作为参数提供的属性值应用到该方法。
Kotlin 覆盖属性
Kotlin 允许声明属性覆盖来改变子类中的属性。
代码语言:javascript复制open class SuperType(open var field: Int)
class SubType(override var field: Int = 1) :
SuperType(field) {
}
这样的安排呈现了两个名称为 的属性field。Kotlin 为每个类中的每个属性生成属性访问器(getter 和 setter)。实际上,代码如下所示:
代码语言:javascript复制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;
}
}
getter 和 setterSubType只在set 上,SubType.field而不是SuperType.field. 在这种安排中,使用构造函数是设置的唯一默认方法SuperType.field。添加方法 to SubTypeset SuperType.fieldviathis.SuperType.field = …是可能的,但不属于支持的约定。属性覆盖在某种程度上会产生冲突,因为属性共享相同的名称但可能代表两个不同的值。我们通常建议使用不同的属性名称。
Spring Data 模块通常支持包含不同值的覆盖属性。从编程模型的角度来看,需要考虑以下几点:
- 应该保留哪个属性(默认为所有声明的属性)?您可以通过使用 注释这些属性来排除属性@Transient。
- 如何表示数据存储中的属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此您应该使用明确的字段/列名称来注释至少一个属性。
- using@AccessType(PROPERTY)不能使用,因为不能设置超级属性。
9.2.实体映射
Spring Data for Apache Geode 支持映射存储在区域中的实体。映射元数据是通过使用应用程序域类上的注释来定义的,如以下示例所示:
示例 3. 将域类映射到 Apache Geode Region
代码语言:javascript复制@Region("People")
public class Person {
@Id Long id;
String firstname;
String lastname;
@PersistenceConstructor
public Person(String firstname, String lastname) {
// …
}
…
}
该@Region注解可用于自定义Person存储类实例的 Region 。该@Id注解可以用于标注应该被用作缓存区域重点,确定区域项属性。该@PersistenceConstructor注释有助于消除歧义多个潜在可用的构造函数,以参数并明确标识标注为构造函数的构造函数被用来构造实体。在没有或只有一个构造函数的应用程序域类中,您可以省略注释。
除了将实体存储在顶级 Region 中,实体也可以存储在 Sub-Regions 中,如下例所示:
代码语言:javascript复制@Region("/Users/Admin")
public class Admin extends User {
…
}
@Region("/Users/Guest")
public class Guest extends User {
…
}
请务必使用 Apache Geode 区域的完整路径,如使用元素的id或name属性使用 Spring Data for Apache Geode XML 命名空间定义的那样<*-region>。