【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数据结构映射到这些实例上。这意味着我们需要两个基本步骤:

  1. 使用公开的构造函数之一创建实例。
  2. 实例填充以物化(materialize)所有公开的属性。

一、对象创建

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

  1. 如果有一个用@PersistenceCreator注解的静态工厂方法,那么就使用它。
  2. 如果存在单个构造函数,则使用它。
  3. 如果有多个构造函数,并且恰好有一个构造函数是用@PersistenceCreator注解的,则使用它。
  4. 如果类型是Java Record,则使用规范构造函数。
  5. 如果存在无参数构造函数,则使用它。其他构造函数将被忽略。

值解析假定构造函数/工厂方法参数名称与实体的属性名称匹配,即解析将像填充属性一样执行,包括映射中的所有自定义项(不同的数据存储列或字段名等)。这还需要类文件中可用的参数名称信息或构造函数上存在的@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%。对于有资格进行此类优化的域类,它需要遵守一组约束:

  1. 它不能是private class
  2. 它不能是非静态的内部类
  3. 它不能是CGLib代理类
  4. Spring Data要使用的构造函数不能是私有的

如果这些条件中没有任何一个匹配,Spring Data将返回到通过反射实例化实体。

二、属性填充Property population

一旦创建了实体的实例,Spring Data就会填充该类的所有剩余持久属性。除非已经由实体的构造函数填充(即使用其构造函数参数列表),否则将首先填充标识符属性,以允许解析循环对象引用。之后,在实体实例上设置所有尚未由构造函数填充的非瞬态(non-transient)属性。为此,框架使用以下算法:

  1. 如果属性是不可变的,但公开了with…方法(见下文),框架将使用with…方法创建一个具有新属性值的新实体实例。
  2. 如果定义了属性访问(即通过getters和setters进行访问),框架将调用setter方法。
  3. 如果属性是可变的,框架直接设置字段。
  4. 如果属性是不可变的,框架将使用持久性操作(请参见5.1对象创建)使用的构造函数来创建实例的副本。
  5. 默认情况下,框架直接设置字段值。

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模块通常支持具有不同值的重写属性。从编程模型的角度来看,需要考虑以下几点:

  1. 应该持久化哪个属性(默认为所有声明的属性)?可以通过使用@Transient注解特性来排除属性(properties)。
  2. 如何在数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,因此应使用显式字段/列名对至少一个属性进行注解。
  3. 不能使用@AccessType(PROPERTY),因为在不对setter实现进行任何进一步假设的情况下,通常不能设置super-property。

四、Kotlin支持

Spring Data适配了Kotlin的特性,允许对象创建和变化(mutation)。

4.1 Kotlin 对象创建

Kotlin类支持实例化,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。
Spring Data会自动尝试检测用于物化(materialize)该类型对象的持久实体的构造函数。解析算法的工作原理如下:

  1. 如果有一个构造函数是用@PersistenceCreator注解的,那么就会使用它。
  2. 如果类型是Kotlin data cass,则使用主构造函数。
  3. 如果有一个用@PersistenceCreator注解的静态工厂方法,那么就使用它。
  4. 如果存在单个构造函数,则使用它。
  5. 如果有多个构造函数,并且恰好有一个构造函数是用@PersistenceCreator注解的,则使用它。
  6. 如果类型是Java Record,则使用规范构造函数。
  7. 如果存在无参数构造函数,则使用它。其他构造函数将被忽略。

考虑以下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模块通常支持具有不同值的重写属性。从编程模型的角度来看,需要考虑以下几点:

  1. 应该持久化哪个属性(默认为所有声明的属性)?可以通过使用@Transient注解特性来排除这些属性。
  2. 如何在数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,因此应使用显式字段/列名对至少一个属性进行注解。
  3. 不能使用@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类型用其包装器类型表示,这会影响值类型在数据库中的表示方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/710737.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

libigl 网格中点细分(网格上采样)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 在网格细分中,我们可以将每个三角形分成若干个更小的三角形。其中最简单的情况就是通过计算每个三角形每条边的中点,这样就可以将三角形分成四个更小的三角形。不过该方法并不会使得网格的表面和面积发生变化,而…

Python:练习:编写一个程序,写入一个美金数量,然后显示出如何用最少的20美元、10美元、5美元和1美元来付款

案例: python编写一个程序,写入一个美金数量,然后显示出如何用最少的20美元、10美元、5美元和1美元来付款: Enter a dollar amout:93 $20 bills: 4 $10 bills: 1 $5 bills:0 $1 bills:3 思考: 写入一个美金数量&…

数据分析Pandas专栏---第十一章<Pandas数据聚合与分组(1)>

前言: 数据聚合和分组操作是数据处理过程中不可或缺的一部分。它们允许我们根据特定的条件对数据进行分组,并对每个组进行聚合计算。这对于统计分析、汇总数据以及生成报告和可视化非常有用。无论是市场营销数据分析、销售业绩评估还是金融数据建模,数据…

【数据分享】2000~2023年MOD15A2H 061 叶面积指数LAI数据集

各位同学们好,今天和大伙儿交流的是2000~2013年MOD15A2H 061 LAI数据集。如果大家有下载处理数据等方面的问题,您可以私信或评论。 Myneni, R., Y. Knyazikhin, T. Park. MODIS/Terra Leaf Area Index/FPAR 8-Day L4 Global 500m SIN Grid V061. 2021, d…

在原有项目进行业务逻辑开发:同一用户短时间不得提交多次申请,以及更新主表时数据刷新掉了角色权限以及密码重置的问题,详细思路及代码

开发背景: 用户提交表单后,插入到对应数据库表的字段中去,因需要保存是哪一个用户提交的,所以需要拿到主表的user_id,更新功能为记录提交时间,短时间不得再次提交 在对一个已有角色权限分配,登录…

【Spring连载】使用Spring Data访问 MongoDB----对象映射之对象引用

【Spring连载】使用Spring Data访问 MongoDB----对象映射之对象引用 一、使用DBRefs 一、使用DBRefs

layui中,父页面与子页面,函数方法的相互调用、传参

<%--父页面--%> <script type"text/javascript">var KaoHaoType 0; // 考号类型 自定义参数1// 选取考号类型function SelectKaoHaoType(callBack) {KaoHaoType 0; // 默认选择填涂考号layer.open({type: 2, title: 请选择 考号区类型, ar…

职场中被小人欺负了,应该一笑了之吗?还是怎么办?

在职场中遇到不公正的待遇或被欺负&#xff0c;确实是一个让人困扰的问题。处理这类问题&#xff0c;首先要保持冷静和理性&#xff0c;避免情绪化的反应&#xff0c;这样有助于找到最合适的解决方案。以下是一些建议&#xff0c;您可以根据具体情况考虑&#xff1a; 1. **保持…

如何使用 Socket.IO、Angular 和 Node.js 创建实时应用程序

介绍 WebSocket 是一种允许服务器和客户端之间进行全双工通信的互联网协议。该协议超越了典型的 HTTP 请求和响应范式。通过 WebSocket&#xff0c;服务器可以向客户端发送数据&#xff0c;而无需客户端发起请求&#xff0c;因此可以实现一些非常有趣的应用程序。 在本教程中…

网络编程作业day2

1.将TPC和UDP通信模型各敲两遍 &#xff08;1&#xff09;TPC通信模型&#xff1a; 服务器代码&#xff1a; #include <myhead.h> #define SERVER_IP "192.168.125.136" #define SERVER_PORT 1314 int main(int argc, const char *argv[]) {//1、创建用于监…

CLion 2023:专注于C和C++编程的智能IDE mac/win版

JetBrains CLion 2023是一款专为C和C开发者设计的集成开发环境&#xff08;IDE&#xff09;&#xff0c;它集成了许多先进的功能&#xff0c;旨在提高开发效率和生产力。 CLion 2023软件获取 CLion 2023的智能代码编辑器提供了丰富的代码补全和提示功能&#xff0c;使您能够更…

统计业务流量的毫秒级峰值 - 华为机试真题题解

考试平台&#xff1a; 时习知 分值&#xff1a; 200分&#xff08;第二题&#xff09; 考试时间&#xff1a; 两小时&#xff08;共3题&#xff09; 题目描述 业务模块往外发送报文时&#xff0c;有时会出现网卡队列满而丢包问题&#xff0c;但从常规的秒级流量统计结果看&…

Mybatis-Plus介绍

目录 一、Mybatis-Plus简介 1.1、介绍 1.2、特性 1.3、架构 1.4、Mybatis-Plus与Mybatis的区别 二、快速入门 2.1、首先创建数据库mybatis-plus 2.2、创建user表 2.3、插入数据 2.4、创建Spring-Boot项目 2.5、添加依赖 2.6、连接数据库 一、Mybatis-Plus简介 1.1、…

代码随想录第46天|139.单词拆分 多重背包理论基础 背包总结

文章目录 单词拆分思路&#xff1a;代码 多重背包≈0-1背包题目代码 背包总结 单词拆分 3 思路&#xff1a; 代码 class Solution {public boolean wordBreak(String s, List<String> wordDict) {HashSet<String> set new HashSet<>(wordDict);boolean[]…

15个非常实用的JavaScript技巧,提高你的开发效率

本文我们将探讨15个实用的JavaScript技巧&#xff0c;希望它们可以帮你提高开发效率&#xff0c;有用的话点赞收藏~。 1. 反转字符串 你有时候可能需要将字符串颠倒过来。在JavaScript中&#xff0c;有一个巧妙的一行代码可以实现这个目标&#xff1a; const reversedString…

sheng的学习笔记-卷积神经网络经典架构-LeNet-5、AlexNet、VGGNet-16

目录&#xff1a;目录 看本文章之前&#xff0c;需要学习卷积神经网络基础&#xff0c;可参考 sheng的学习笔记-卷积神经网络-CSDN博客 目录 LeNet-5 架构图 层级解析 1、输入层&#xff08;Input layer&#xff09; 2、卷积层C1&#xff08;Convolutional layer C1&…

Dockerfile(5) - CMD 指令详解

CMD 指定容器默认执行的命令 # exec 形式&#xff0c;推荐 CMD ["executable","param1","param2"] CMD ["可执行命令", "参数1", "参数2"...]# 作为ENTRYPOINT的默认参数 CMD ["param1","param…

VUE3自定义文章排行榜的简单界面

文章目录 一、代码展示二、代码解读三、结果展示 一、代码展示 <template><div class"article-ranking"><div class"header"><h2 class"title">{{ title }}</h2></div><div class"ranking-list&qu…

根据A(String)字段去重,并且选择B(Ingter)字段最大的值

数据格式&#xff1a; [SkillDTO(Job电线工, rankGrade高级工, r4), SkillDTO(Job监察员, rankGrade技师, r5), SkillDTO(Job监察员, rankGrade高级工, r4), SkillDTO(skillJob监察员, rankGrade中级工, r3)] List<SkillDTO> resultList SkillDTOList.stream().coll…

电子技术——PN结电流关系方程

电子技术——PN结电流关系方程 平衡状态下的PN结 平衡状态下的PN结界面总共有两种电流&#xff0c;一种为 扩散电流 另一种为 漂移电流 。两种电流形成的平衡区域称为 耗散区 。 在平衡状态扩散电流等于漂移电流&#xff0c;此时静电流为0&#xff0c;PN结外部没有电流&…