本文主要是介绍【Spring连载】使用Spring Data----对象映射基础Object Mapping Fundamentals,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
【Spring连载】使用Spring Data----对象映射基础Object Mapping Fundamentals
- 一、对象创建
- 1.1 对象创建内部机制Object creation internals
- 二、属性填充Property population
- 2.1 属性填充内部机制Property population internals
- 三、一般建议
- 3.1 覆盖属性
- 四、Kotlin支持
- 4.1 Kotlin 对象创建
- 4.2 Kotlin data 类的属性填充
- 4.3 Kotlin 覆盖属性
- 4.4 Kotlin Value 类
本节介绍Spring Data对象映射、对象创建、字段和属性访问、可变性和不变性的基本原理。请注意,本节仅适用于不使用底层数据存储(如JPA)的对象映射的Spring Data模块。此外,请务必了解特定于存储对象的映射,如索引、自定义列名或字段名等。
SpringData对象映射的核心职责是创建域对象的实例,并将store-native数据结构映射到这些实例上。这意味着我们需要两个基本步骤:
- 使用公开的构造函数之一创建实例。
- 实例填充以物化(materialize)所有公开的属性。
一、对象创建
Spring Data会自动尝试检测用于物化该类型对象的持久实体的构造函数。解析算法的工作原理如下:
- 如果有一个用@PersistenceCreator注解的静态工厂方法,那么就使用它。
- 如果存在单个构造函数,则使用它。
- 如果有多个构造函数,并且恰好有一个构造函数是用@PersistenceCreator注解的,则使用它。
- 如果类型是Java Record,则使用规范构造函数。
- 如果存在无参数构造函数,则使用它。其他构造函数将被忽略。
值解析假定构造函数/工厂方法参数名称与实体的属性名称匹配,即解析将像填充属性一样执行,包括映射中的所有自定义项(不同的数据存储列或字段名等)。这还需要类文件中可用的参数名称信息或构造函数上存在的@ConstructorProperties注解。
值解析可以通过使用Spring Framework的@Value值注解(使用特定于存储的SpEL表达式)进行自定义。有关更多详细信息,请参阅有关特定于存储的映射的部分。
1.1 对象创建内部机制Object creation internals
为了避免反射的开销,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%。对于有资格进行此类优化的域类,它需要遵守一组约束:
- 它不能是private class
- 它不能是非静态的内部类
- 它不能是CGLib代理类
- Spring Data要使用的构造函数不能是私有的
如果这些条件中没有任何一个匹配,Spring Data将返回到通过反射实例化实体。
二、属性填充Property population
一旦创建了实体的实例,Spring Data就会填充该类的所有剩余持久属性。除非已经由实体的构造函数填充(即使用其构造函数参数列表),否则将首先填充标识符属性,以允许解析循环对象引用。之后,在实体实例上设置所有尚未由构造函数填充的非瞬态(non-transient)属性。为此,框架使用以下算法:
- 如果属性是不可变的,但公开了with…方法(见下文),框架将使用with…方法创建一个具有新属性值的新实体实例。
- 如果定义了属性访问(即通过getters和setters进行访问),框架将调用setter方法。
- 如果属性是可变的,框架直接设置字段。
- 如果属性是不可变的,框架将使用持久性操作(请参见5.1对象创建)使用的构造函数来创建实例的副本。
- 默认情况下,框架直接设置字段值。
2.1 属性填充内部机制Property population internals
与对象构造方面的优化(5.1.1章节)类似,框架也使用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使用字段访问来读取和写入属性值。根据私有字段的可见性规则,MethodHandles用于与字段交互。
3. 该类公开了一个用于设置标识符的withId(…)方法,例如,当一个实例插入到数据存储中并生成了标识符时。调用withId(…)将创建一个新的Person对象。所有后续的变化(mutations)都将发生在新的实例中,而不影响先前的实例。
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) { --------6return new Person(null, firstname, lastname, birthday,Period.between(birthday, LocalDate.now()).getYears());}Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { --------6this.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. identifier属性是final,但在构造函数中设置为null。该类公开了一个用于设置标识符的withId(…)方法,例如,当一个实例插入到数据存储中并生成了标识符时。原始Person实例在创建新实例时保持不变。同样的模式通常应用于存储管理的其他属性,但可能必须更改这些属性才能进行持久性操作。wither方法是可选的,因为持久性构造函数(请参见6)实际上是一个复制构造函数,设置属性将转化为创建一个应用了新标识符值的新实例。
2. firstname和lastname属性是通过getter公开的普通不可变属性。
3. age属性是不可变的,但派生自birthday属性。在显示的设计中,数据库值将胜过默认值,因为Spring Data使用了唯一声明的构造函数。即使目的是首选(preferred)计算,重要的是该构造函数也要将年龄作为参数(可能会忽略它),否则属性填充步骤将试图设置年龄字段,但由于它是不可变的,并且不存在with…方法,因此失败。
4. comment属性是可变的,可以通过直接设置其字段来填充。
5. remarks属性是可变的,并且通过调用setter方法来填充。
6. 该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是额外的构造函数,以避免通过@PersistenceCreator消除构造函数的歧义。相反,属性的默认设置是在工厂方法中处理的。如果你希望Spring Data使用工厂方法进行对象实例化,请使用@PersistenceCreator对其进行注解。
三、一般建议
- 尽量坚持使用不可变的对象——创建不可变对象很简单,因为物化(materializing )对象只需调用其构造函数。此外,这避免了域对象中充斥着允许客户端代码操作对象状态的setter方法。如果你需要这些,最好让它们受到包保护,这样它们只能由有限数量的共存(co-located)类型调用。仅构造函数的物化比属性填充快30%。
- 提供一个包含全部参数的构造函数——即使你不能或不想将实体塑造(model)为不可变的值,提供一个将实体的所有属性(包括可变属性)作为参数的构造函数仍然有价值,因为这允许对象映射跳过属性填充以获得最佳性能。
- 使用工厂方法而不是重载构造函数来避免@PersistenceCreator——对于优化性能所需的全参数构造函数,我们通常希望公开更多特定于应用程序用例的构造函数,这些构造函数省略了自动生成的标识符等。使用静态工厂方法来公开全参数构造函数的这些变体是一种既定模式。
- 确保遵守允许使用生成的实例化器和属性访问器类的约束——
- 对于要生成的标识符,仍然将final字段与all-arguments持久性构造函数(首选)或with…方法结合使用——
- 使用Lombok避免样板(boilerplate)代码 — 由于持久性操作通常需要构造函数接受所有参数,因此它们的声明变成了样板参数到字段赋值的乏味重复,最好使用Lombok的@AllArgsConstructor来避免这种情况。
3.1 覆盖属性
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;}@Overridepublic String getField() {return this.field;}public void setField(String field) {this.field = field;// optionalsuper.setField(field);}
}
这两个类都使用可赋值类型定义字段。然而,SubType隐藏了SuperType.field。根据类设计,使用构造函数可能是设置SuperType.field的唯一默认方法。或者,在setter中调用super.setField(…)可以在SuperType中设置字段。所有这些机制都会在一定程度上造成冲突,因为属性共享相同的名称,但可能表示两个不同的值。如果类型不可赋值,则Spring Data将跳过super-type属性。也就是说,被重写属性的类型必须可分配给要注册为重写的super-type属性类型,否则该超类型属性被视为瞬态属性。我们通常建议使用不同的属性名称。
Spring Data模块通常支持具有不同值的重写属性。从编程模型的角度来看,需要考虑以下几点:
- 应该持久化哪个属性(默认为所有声明的属性)?可以通过使用@Transient注解特性来排除属性(properties)。
- 如何在数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,因此应使用显式字段/列名对至少一个属性进行注解。
- 不能使用@AccessType(PROPERTY),因为在不对setter实现进行任何进一步假设的情况下,通常不能设置super-property。
四、Kotlin支持
Spring Data适配了Kotlin的特性,允许对象创建和变化(mutation)。
4.1 Kotlin 对象创建
Kotlin类支持实例化,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。
Spring Data会自动尝试检测用于物化(materialize)该类型对象的持久实体的构造函数。解析算法的工作原理如下:
- 如果有一个构造函数是用@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) {@PersistenceCreatorconstructor(id: String) : this(id, "unknown")
}
Kotlin通过允许在未提供参数的情况下使用默认值来支持参数可选性。当Spring Data检测到具有参数默认值的构造函数时,如果数据存储不提供值(或只是返回null),则它将忽略这些参数,因此Kotlin可以应用参数默认值。参见以下类,该类将参数默认值应用于name
data class Person(var id: String, val name: String = "unknown")
每当name参数不是结果的一部分或其值为空时,则name默认为unknown。
4.2 Kotlin data 类的属性填充
在Kotlin中,所有的类在默认情况下都是不可变的,并且需要显式的属性声明来定义可变属性。参见以下data class Person:
data class Person(val id: String, val name: String)
这个类实际上是不可变的。它允许创建新实例,因为Kotlin生成一个copy(…)方法,该方法创建新对象实例,从现有对象复制所有属性值,并应用提供的属性值作为方法的参数。
4.3 Kotlin 覆盖属性
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上的Getters和setters只设置SubType.field,而不设置SuperType.field。在这种安排中,使用构造函数是设置SuperType.field的唯一默认方法。可以通过“this.SuperType.field=…”将方法添加到SubType以设置“SuperType.field”,但不在支持的约定范围内。属性重写在一定程度上会造成冲突,因为属性共享相同的名称,但可能表示两个不同的值。我们通常建议使用不同的属性名称。
Spring Data模块通常支持具有不同值的重写属性。从编程模型的角度来看,需要考虑以下几点:
- 应该持久化哪个属性(默认为所有声明的属性)?可以通过使用@Transient注解特性来排除这些属性。
- 如何在数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,因此应使用显式字段/列名对至少一个属性进行注解。
- 不能使用@AccessType(PROPERTY),因为不能设置super-property。
4.4 Kotlin Value 类
Kotlin Value类是为更具表现力的领域模型(domain model)而设计的,以使底层概念易于理解。Spring Data可以读取和写入使用Value类定义属性的类型。
参见以下领域模型:
@JvmInline
value class EmailAddress(val theAddress: String) --------1 data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) ---21. 具有不可为null的值类型的简单value类。
2. 使用EmailAddress值类定义属性的Data class。
使用非基本值类型的非空属性在编译类中被展平(flattened)为value类型。可空的原始值类型或可空的value-in-value类型用其包装器类型表示,这会影响值类型在数据库中的表示方式。
这篇关于【Spring连载】使用Spring Data----对象映射基础Object Mapping Fundamentals的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!