映射
MappingR2dbcConverter 提供了丰富的映射支持。MappingR2dbcConverter 拥有一个丰富的元数据模型,可用于将领域对象映射到数据行。
该映射元数据模型通过在您的领域对象上使用注解进行填充。
然而,该基础设施并不仅限于使用注解作为元数据信息的唯一来源。
MappingR2dbcConverter 还允许您遵循一组约定,在不提供任何额外元数据的情况下将对象映射到数据行。
本节介绍 MappingR2dbcConverter 的功能,包括如何使用约定将对象映射到行,以及如何通过基于注解的映射元数据来覆盖这些约定。
在继续阅读本章之前,请先了解对象映射基础的相关内容。
基于约定的映射
MappingR2dbcConverter 在未提供额外映射元数据时,有一些将对象映射到行的约定。
这些约定如下:
-
短 Java 类名按以下方式映射到表名。
com.bigbank.SavingsAccount类映射为SAVINGS_ACCOUNT表名。 同样的名称映射规则也适用于将字段映射到列名。 例如,firstName字段映射为FIRST_NAME列。 你可以通过提供自定义的NamingStrategy来控制此映射行为。 更多详细信息,请参见映射配置。 默认情况下,从属性名或类名派生出的表名和列名在 SQL 语句中不使用引号。 你可以通过设置RelationalMappingContext.setForceQuote(true)来控制这一行为。 -
不支持嵌套对象。
-
该转换器使用通过
CustomConversions注册的任何 Spring 转换器,以覆盖对象属性到行的列和值的默认映射。 -
对象的字段用于在行中的列之间进行转换。 不会使用公共的
JavaBean属性。 -
如果你有一个唯一的非零参数构造函数,且其构造函数参数名称与行的顶层列名匹配,则使用该构造函数。 否则,将使用无参构造函数。 如果存在多个非零参数构造函数,则会抛出异常。 更多详细信息,请参阅对象创建。
映射配置
默认情况下(除非显式配置),在创建 MappingR2dbcConverter 时会自动创建一个 DatabaseClient 实例。
您可以创建自己的 MappingR2dbcConverter 实例。
通过创建自己的实例,您可以注册 Spring 转换器,用于将特定类与数据库之间进行映射。
你可以使用基于 Java 的元数据来配置 MappingR2dbcConverter、DatabaseClient 以及 ConnectionFactory。
以下示例使用了 Spring 的基于 Java 的配置:
如果将 setForceQuote 的 R2dbcMappingContext to 设置为 true,则从类和属性派生出的表名和列名将使用数据库特定的引号。
这意味着在这些名称中使用 SQL 保留字(例如 order)是安全的。
您可以通过重写 r2dbcMappingContext(Optional<NamingStrategy>) 中的 AbstractR2dbcConfiguration 方法来实现这一点。
当未使用引号时,Spring Data 会将此类名称的字母大小写转换为所配置数据库通常使用的格式。
因此,只要您的名称中不包含关键字或特殊字符,创建表时就可以使用不带引号的名称。
对于遵循 SQL 标准的数据库,这意味着名称会被转换为大写形式。
引号字符以及名称的大写方式由所使用的 R2dbcDialect 控制。
有关如何配置自定义方言,请参见R2DBC 驱动程序。
@Configuration
public class MyAppConfig extends AbstractR2dbcConfiguration {
public ConnectionFactory connectionFactory() {
return ConnectionFactories.get("r2dbc:…");
}
// the following are optional
@Override
protected List<Object> getCustomConverters() {
return List.of(new PersonReadConverter(), new PersonWriteConverter());
}
}
AbstractR2dbcConfiguration 要求您实现一个用于定义 ConnectionFactory 的方法。
你可以通过重写 r2dbcCustomConversions 方法向转换器中添加额外的转换器。
你可以通过将其注册为一个 Bean 来配置自定义的 NamingStrategy。
NamingStrategy 控制类名和属性名如何转换为表名和列名。
AbstractR2dbcConfiguration 创建一个 DatabaseClient 实例,并将其以 databaseClient 的名称注册到容器中。 |
基于元数据的映射
为了充分利用 Spring Data R2DBC 支持中的对象映射功能,您应该使用 @Table 注解来标注您的映射对象。
尽管映射框架并不强制要求此注解(即使没有任何注解,您的 POJO 也能被正确映射),但它能让类路径扫描器发现并预处理您的领域对象,以提取必要的元数据。
如果您不使用此注解,那么在首次存储领域对象时,应用程序会受到轻微的性能影响,因为映射框架需要构建其内部元数据模型,以便了解您的领域对象的属性以及如何持久化它们。
以下示例展示了一个领域对象:
@Table
public class Person {
@Id
private Long id;
private Integer ssn;
private String firstName;
private String lastName;
}
@Id 注解告诉映射器你想将哪个属性用作主键。 |
默认类型映射
下表说明了实体的属性类型如何影响映射:
| 源类型 | 目标类型 | 备注 |
|---|---|---|
基本类型和包装类型 |
透传 |
可以使用显式转换器进行自定义。 |
JSR-310 日期/时间类型 |
透传 |
可以使用显式转换器进行自定义。 |
|
透传 |
可以使用显式转换器进行自定义。 |
|
字符串 |
可通过注册显式转换器进行自定义。 |
|
透传 |
可以使用显式转换器进行自定义。 |
|
透传 |
被视为二进制有效载荷。 |
|
|
如果配置的驱动程序支持,则转换为数组类型;否则不支持。 |
基本类型数组、包装类型数组和 |
包装类型数组(例如 |
如果配置的驱动程序支持,则转换为数组类型;否则不支持。 |
特定于驱动程序的类型 |
透传 |
由所使用的 |
复杂对象 |
目标类型取决于已注册的 |
需要一个显式转换器,否则不支持。 |
| 列的原生数据类型取决于 R2DBC 驱动程序的类型映射。 驱动程序可以提供额外的简单类型,例如几何(Geometry)类型。 |
映射注解概述
RelationalConverter 可以使用元数据来驱动对象到行的映射。
以下注解可用:
-
@Embedded:带有此注解的属性将映射到父实体的表中,而不是单独的表。 允许指定生成的列是否应具有一个公共前缀。 如果此类实体生成的所有列均为null,则根据null的值,被注解的实体将为null或empty(即其所有属性均为@Embedded.onEmpty())。 可与@Id结合使用以构成复合主键。 -
@Id:应用于字段级别,用于标记主键。 它可以与@Embedded结合使用,以构成复合主键。 -
@InsertOnlyProperty:将一个属性标记为仅在插入时写入。 聚合根上的此类属性仅会被写入一次,之后永远不会更新。 请注意,在嵌套实体上,所有保存操作都会导致插入操作,因此该注解对嵌套实体的属性无效。 -
@MappedCollection:允许配置集合或单个嵌套实体的映射方式。idColumn指定用于引用父实体主键的列。keyColumn指定用于存储List的索引或Map的键的列。 -
@Sequence:为被注解的属性指定一个数据库序列,用于生成值。 -
@Table:在类级别上使用,用于表明该类是映射到数据库的候选对象。 您可以指定存储该数据库表的名称。 -
@Transient:默认情况下,所有字段都会映射到数据库行中。 此注解用于将其所标注的字段排除在数据库存储之外。 瞬态(Transient)属性不能在持久化构造函数中使用,因为转换器无法为构造函数参数生成一个值。 -
@PersistenceCreator:标记一个给定的构造函数或静态工厂方法(即使是包级私有的)在从数据库实例化对象时使用。 构造函数参数通过名称映射到所检索行中的值。 -
@Value:此注解是 Spring 框架的一部分。 在映射框架中,它可以应用于构造函数参数。 这允许你使用 Spring 表达式语言(SpEL)语句,在从数据库检索到某个键的值之后、用于构造领域对象之前对其进行转换。 为了引用给定行中的某一列,必须使用类似如下的表达式:@Value("#root.myProperty"),其中 root 指向给定Row的根。 -
@Column:应用于字段级别,用于描述该字段在数据库行中所对应的列名,允许列名与类中的字段名不同。 使用@Column注解指定的名称在 SQL 语句中始终会被加上引号。 对于大多数数据库而言,这意味着这些名称是区分大小写的。 这也意味着你可以在这些名称中使用特殊字符。 然而,我们不建议这样做,因为这可能会与其他工具产生兼容性问题。 -
@Version:应用于字段级别,用于乐观锁,并在保存操作时检查是否发生修改。 值为null(对于基本类型为zero)被视为新实体的标记。 初始存储的值为zero(对于基本类型为one)。 版本号会在每次更新时自动递增。 更多详情请参阅乐观锁。
映射元数据基础设施定义在独立的、与技术无关的 spring-data-commons 项目中。
R2DBC 支持中使用了特定的子类来支持基于注解的元数据。
也可以根据需求引入其他策略。
命名策略
按照约定,Spring Data 应用 NamingStrategy 来确定表、列和模式名称,默认采用 蛇形命名法(snake case)。
名为 firstName 的对象属性将变为 first_name。
您可以在应用程序上下文中提供 NamingStrategy 来调整这一行为。
覆盖表名
当表命名策略与您的数据库表名不匹配时,您可以使用 Table 注解覆盖表名。
该注解的元素 value 提供自定义表名。
以下示例将 MyEntity 类映射到数据库中的 CUSTOM_TABLE_NAME 表:
@Table("CUSTOM_TABLE_NAME")
class MyEntity {
@Id
Integer id;
String name;
}
您可以使用Spring Data 的 SpEL 支持来动态创建表名。 一旦生成,表名将被缓存,因此它仅在每个映射上下文中是动态的。
覆盖列名
当列命名策略与您的数据库表名不匹配时,您可以使用 Column 注解来覆盖表名。
该注解的 value 元素提供自定义列名。
以下示例将 MyEntity 类的 name 属性映射到数据库中的 CUSTOM_COLUMN_NAME 列:
class MyEntity {
@Id
Integer id;
@Column("CUSTOM_COLUMN_NAME")
String name;
}
您可以使用Spring Data 的 SpEL 支持来动态创建列名。 一旦生成,这些名称将被缓存,因此仅在每个映射上下文中具有动态性。
嵌入的 ID
标识符属性可以使用 @Embedded 注解,从而允许使用复合主键(composite ids)。
整个嵌入的实体被视为 ID,因此在判断一个聚合是新聚合(需要执行插入操作)还是已有聚合(需要执行更新操作)时,依据的是该实体本身,而不是其内部的各个元素。
大多数使用场景都需要自定义一个 BeforeConvertCallback,以便为新的聚合设置 ID。
@Table("PERSON_WITH_COMPOSITE_ID")
record Person( (1)
@Id @Embedded.Nullable Name pk, (2)
String nickName,
Integer age
) {
}
record Name(String first, String last) {
}
CREATE TABLE PERSON_WITH_COMPOSITE_ID (
FIRST VARCHAR(100),
LAST VARCHAR(100),
NICK_NAME VARCHAR(100),
AGE INT,
PRIMARY KEY (FIRST, LAST) (3)
);
| 1 | 实体可以表示为记录,而无需任何特殊考虑。 |
| 2 | pk 被标记为 id 并嵌入 |
| 3 | 嵌入的Name实体中的两个列构成了数据库中的主键。 |
表的创建细节取决于所使用的数据库。
只读属性
使用 @ReadOnlyProperty 注解的属性不会被 Spring Data 写入数据库,但在加载实体时会被读取。
Spring Data 在写入实体后不会自动重新加载该实体。 因此,如果你希望看到数据库为此类列生成的数据,必须显式地重新加载它。
如果被注解的属性是一个实体或实体集合,则它在单独的表中由一行或多行表示。 Spring Data 不会对这些行执行任何插入、删除或更新操作。
仅插入属性
使用 @InsertOnlyProperty 注解的属性仅在插入操作期间由 Spring Data 写入数据库。
在更新操作中,这些属性将被忽略。
@InsertOnlyProperty 仅支持聚合根。
自定义对象构建
映射子系统允许通过使用 @PersistenceConstructor 注解标注构造函数来定制对象的构造方式。构造函数参数所使用的值将按以下方式解析:
-
如果一个参数使用
@Value注解进行标注,则会计算给定的表达式,并将结果用作该参数的值。 -
如果 Java 类型具有一个属性,其名称与输入行中的给定字段相匹配,那么将使用该属性的信息来选择合适的构造函数参数,以传递输入字段的值。 这仅在 Java
.class文件中包含参数名称信息时才有效,您可以通过使用调试信息编译源代码,或在 Java 8 中使用-parameters的javac命令行选项来实现这一点。 -
否则,将抛出一个
MappingException,以表明给定的构造函数参数无法被绑定。
class OrderItem {
private @Id final String id;
private final int quantity;
private final double unitPrice;
OrderItem(String id, int quantity, double unitPrice) {
this.id = id;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
// getters/setters omitted
}
使用显式转换器覆盖映射
在存储和查询对象时,通常使用一个 R2dbcConverter 实例来处理所有 Java 类型到 OutboundRow 实例的映射会非常方便。
然而,有时你可能希望 R2dbcConverter 实例完成大部分工作,但又能让你针对特定类型有选择性地处理转换——例如为了优化性能。
要选择性地自行处理转换,请向 org.springframework.core.convert.converter.Converter 注册一个或多个 R2dbcConverter 实例。
您可以使用 r2dbcCustomConversions 中的 AbstractR2dbcConfiguration 方法来配置转换器。
本章开头的示例展示了如何使用 Java 进行配置。
自定义顶层实体转换需要使用非对称类型进行转换。
入站数据从 R2DBC 的 Row 中提取。
出站数据(用于 INSERT/UPDATE 语句)以 OutboundRow 形式表示,并在后续组装为 SQL 语句。 |
以下是一个 Spring Converter 实现的示例,它将 Row 转换为 Person POJO:
@ReadingConverter
public class PersonReadConverter implements Converter<Row, Person> {
public Person convert(Row source) {
Person p = new Person(source.get("id", String.class),source.get("name", String.class));
p.setAge(source.get("age", Integer.class));
return p;
}
}
请注意,转换器会应用于单个属性。
集合属性(例如 Collection<Person>)会被逐个元素迭代并进行转换。
不支持集合转换器(例如 Converter<List<Person>>, OutboundRow)。
R2DBC 使用包装类型(Integer.class 而不是 int.class)来返回基本类型的值。 |
以下示例将 Person 转换为 OutboundRow:
@WritingConverter
public class PersonWriteConverter implements Converter<Person, OutboundRow> {
public OutboundRow convert(Person source) {
OutboundRow row = new OutboundRow();
row.put("id", Parameter.from(source.getId()));
row.put("name", Parameter.from(source.getFirstName()));
row.put("age", Parameter.from(source.getAge()));
return row;
}
}
使用显式转换器覆盖枚举映射
某些数据库(例如 PostgreSQL)可以使用其特定于数据库的枚举列类型原生地写入枚举值。
Spring Data 默认会将 Enum 值转换为 String 值,以实现最大程度的可移植性。
若要保留实际的枚举值,请注册一个 @Writing 转换器,该转换器的源类型和目标类型均使用实际的枚举类型,从而避免使用 Enum.name() 进行转换。
此外,您还需要在驱动程序级别配置枚举类型,以便驱动程序知道如何表示该枚举类型。
以下示例展示了用于原生读取和写入 Color 枚举值所涉及的组件:
enum Color {
Grey, Blue
}
class ColorConverter extends EnumWriteSupport<Color> {
}
class Product {
@Id long id;
Color color;
// …
}