跳到主要内容

映射

Doubao 中英对照 Mapping

MappingR2dbcConverter 提供了丰富的映射支持。MappingR2dbcConverter 拥有丰富的元数据模型,可将领域对象映射到数据行。映射元数据模型通过在领域对象上使用注解来填充。不过,该基础设施并不限于将注解作为元数据信息的唯一来源。MappingR2dbcConverter 还允许您在不提供任何额外元数据的情况下,通过遵循一组约定将对象映射到行。

本节介绍 MappingR2dbcConverter 的特性,包括如何使用约定将对象映射到行,以及如何使用基于注解的映射元数据来覆盖这些约定。

在继续阅读本章之前,请先阅读对象映射基础。

基于约定的映射​

当未提供额外映射元数据时,MappingR2dbcConverter 有若干将对象映射到行的约定。约定如下:

  • Java 简短类名按如下方式映射为表名。com.bigbank.SavingsAccount 类映射到 SAVINGS_ACCOUNT 表名。将字段映射为列名时也应用相同的名称映射。例如,firstName 字段映射到 FIRST_NAME 列。您可以通过提供自定义 NamingStrategy 来控制此映射。更多细节请参阅映射配置。从属性名或类名派生出来的表名和列名,在 SQL 语句中默认带引号使用。您可以通过设置 R2dbcMappingContext.setForceQuote(false) 来控制此行为,或使用 R2dbcMappingContext.forPlainIdentifiers() 创建一个使用普通(不带引号)标识符的上下文。

  • 约定不支持任意嵌套的对象。对于映射到同一张表中列的值对象,请使用 @Embedded——请参阅内嵌实体。

  • 转换器会使用注册在 CustomConversions 中的任何 Spring 转换器,来覆盖对象属性到行列和行值的默认映射。

  • 对象的字段用于与行中的列进行相互转换。不使用公共 JavaBean 属性。

  • 如果您有唯一一个非零参数构造函数,且其构造函数参数名与行的顶层列名匹配,则使用该构造函数。否则使用零参数构造函数。如果存在多个非零参数构造函数,则抛出异常。进一步细节请参阅对象创建。

映射配置​

默认情况下(除非显式配置),在您创建 DatabaseClient 时会创建一个 MappingR2dbcConverter 实例。您可以创建自己的 MappingR2dbcConverter 实例。通过创建自己的实例,您可以注册 Spring 转换器,以在数据库之间映射特定的类。

您可以使用基于 Java 的元数据来配置 MappingR2dbcConverter 以及 DatabaseClient 和 ConnectionFactory。以下示例使用 Spring 的基于 Java 的配置:

默认情况下,从类和属性派生出来的表名和列名会使用数据库特定的引号,这意味着它们通常是区分大小写的,并且保留字 SQL 词(例如 order)可以安全地用于名称中。要禁用引号,请在 AbstractR2dbcConfiguration 中覆盖 r2dbcMappingContext(Optional<NamingStrategy>) 并返回 R2dbcMappingContext.forPlainIdentifiers(…),或在上下文上调用 context.setForceQuote(false)。禁用引号后,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());
}
}
java

AbstractR2dbcConfiguration 要求您实现一个定义 ConnectionFactory 的方法。

您可以通过覆盖 getCustomConverters 方法向转换器添加额外的转换器。

您可以通过将自定义 NamingStrategy 注册为 bean 来配置它。NamingStrategy 控制类名和属性名如何转换为表名和列名。

备注

AbstractR2dbcConfiguration 会创建一个 DatabaseClient 实例,并以 databaseClient 为名注册到容器中。

基于元数据的映射​

要充分利用 Spring Data R2DBC 支持中的对象映射功能,您应该用 @Table 注解标注您的被映射对象。虽然映射框架并不要求必须有此注解(即使没有任何注解,您的 POJO 也能被正确映射),但它让 classpath 扫描器能够找到并预处理您的领域对象,以提取必要的元数据。如果您不使用此注解,那么在您第一次存储领域对象时,应用程序会有轻微的性能损失,因为映射框架需要构建其内部元数据模型,以便了解您领域对象的属性以及如何持久化它们。以下示例展示了一个领域对象:

@Table
public class Person {

@Id
private Long id;

private Integer ssn;

private String firstName;

private String lastName;
}
java
important

@Id 注解告诉映射器您想将哪个属性用作主键。

默认类型映射​

下表说明实体的属性类型如何影响映射:

源类型目标类型备注
基本类型与包装类型直通可使用显式转换器进行自定义。
JSR-310 日期/时间类型直通可使用显式转换器进行自定义。
String、BigInteger、BigDecimal 和 UUID直通可使用显式转换器进行自定义。
EnumString可通过注册显式转换器进行自定义。
Blob 和 Clob直通可使用显式转换器进行自定义。
byte[]、ByteBuffer直通视为二进制载荷。
Collection<T>T 的数组若所配置的驱动程序支持,则转换为数组类型,否则不支持。
基本类型、包装类型和 String 的数组包装类型的数组(例如 int[] → Integer[])若所配置的驱动程序支持,则转换为数组类型,否则不支持。
驱动程序特定类型直通由所使用的 R2dbcDialect 作为简单类型提供。
复杂对象目标类型取决于已注册的 Converter。需要显式转换器,否则不支持。
备注

列的原生数据类型取决于 R2DBC 驱动程序的类型映射。驱动程序可以提供额外的简单类型,例如 Geometry 类型。

映射注解概述​

RelationalConverter 可以使用元数据来驱动对象到行的映射。可用的注解如下:

  • @Embedded: 带有此注解的属性将被映射到父实体的表,而不是单独的表。允许指定生成的列是否应具有共同前缀。如果由此类实体产生的所有列都为 null,则被注解的实体将为 null 或 empty(即其所有属性都为 null),具体取决于 @Embedded.onEmpty() 的值。可与 @Id 组合以构成复合 id。

  • @Id: 应用于字段级别,用于标记主键。它可以与 @Embedded 组合以构成复合 id。

  • @InsertOnlyProperty: 将一个属性标记为仅在插入期间写入。聚合根上的此类属性只会被写入一次,永不再更新。请注意,在嵌套实体上,所有保存操作都会产生插入,因此此注解对嵌套实体的属性无效。

  • @MappedCollection: 允许配置集合或单个嵌套实体的映射方式。idColumn 指定用于引用父实体主键的列。keyColumn 指定用于存储 List 索引或 Map 键的列。

  • @Sequence: 指定一个数据库序列,用于为被注解的属性生成值。

  • @Table: 应用于类级别,表明该类是映射到数据库的候选者。您可以指定数据库中存储所用表的名称。

  • @Transient: 默认情况下,所有字段都会映射到行。此注解将其标注的字段排除在数据库存储之外。瞬态属性不能用于持久化构造函数中,因为转换器无法为该构造函数参数生成值。

  • @PersistenceCreator: 标记在从数据库实例化对象时要使用的给定构造函数或静态工厂方法——即使是包级私有的也可以。构造函数参数按名称映射到所检索行中的值。

  • @Value: 此注解是 Spring Framework 的一部分。在映射框架中,它可以应用于构造函数参数。这让您可以使用 Spring 表达式语言语句,在数据库中检索到的键值被用于构造领域对象之前对其进行转换。要引用给定行的某一列,必须使用类似这样的表达式:@Value("#root.myProperty"),其中 root 指给定 Row 的根。

  • @Column: 应用于字段级别,用于描述列在行中的表示名称,使该名称可以不同于类的字段名。当启用 forceQuote(默认)时,用 @Column 注解指定的名称会被加引号。对于大多数数据库,加引号意味着这些名称区分大小写。这也意味着您可以在这些名称中使用特殊字符。不过不建议这样做,因为它可能给其他工具带来问题。

  • @Version: 应用于字段级别,用于乐观锁,并在保存操作时检查是否发生修改。包装类型的值为 null(或基本类型为 zero)时将实体标记为新的。初始存储的值对于包装类型为 zero(基本类型为 one)。每次更新时版本都会自动递增。进一步参考请参阅乐观锁。

映射元数据基础设施定义在独立的、与技术无关的 spring-data-commons 项目中。在 R2DBC 支持中使用了特定的子类来支持基于注解的元数据。也可以采用其他策略(如果有需求的话)。

命名策略​

按约定,Spring Data 会应用一个 NamingStrategy 来确定表名、列名和模式名,默认为蛇形命名法。名为 firstName 的对象属性会变成 first_name。您可以通过在应用上下文中提供一个 NamingStrategy 来调整它。

覆盖表名​

当表命名策略与您的数据库表名不匹配时,您可以使用 Table 注解覆盖表名。该注解的 value 元素提供自定义表名。以下示例将 MyEntity 类映射到数据库中的 CUSTOM_TABLE_NAME 表:

@Table("CUSTOM_TABLE_NAME")
class MyEntity {
@Id
Integer id;

String name;
}
java

您可以使用 Spring Data 的 SpEL 支持 动态创建表名。表名一旦生成就会被缓存,因此它仅在每个映射上下文内是动态的。

覆盖列名​

当列命名策略与您的数据库列名不匹配时,您可以使用 Column 注解覆盖列名。该注解的 value 元素提供自定义列名。以下示例将 MyEntity 类的 name 属性映射到数据库中的 CUSTOM_COLUMN_NAME 列:

class MyEntity {
@Id
Integer id;

@Column("CUSTOM_COLUMN_NAME")
String name;
}
java

您可以使用 Spring Data 的 SpEL 支持 动态创建列名。名称一旦生成就会被缓存,因此它仅在每个映射上下文内是动态的。

内嵌实体​

内嵌实体用于在您的 Java 数据模型中拥有值对象,即使您的数据库中只有一张表。在以下示例中,您可以看到 MyEntity 使用 @Embedded 注解进行映射。这样做的结果是:数据库中会期望存在一张 my_entity 表,其中包含 id 和 name(来自 EmbeddedEntity 类)两列。

然而,如果 name 列在结果集中实际为 null,则整个 embeddedEntity 属性会根据 @Embedded 的 onEmpty 被设置为 null——当所有嵌套属性都为 null 时它会将对象置为 null。
与这种行为相反,USE_EMPTY 会尝试使用默认构造函数,或使用一个可接受来自结果集的可空参数值的构造函数来创建新实例。

示例 1. 嵌入对象的示例代码

class MyEntity {

@Id
Integer id;

@Embedded(onEmpty = USE_NULL) 1
EmbeddedEntity embeddedEntity;
}

class EmbeddedEntity {
String name;
}
java
  • 当 name 为 null 时将 embeddedEntity 置为 null。使用 USE_EMPTY 以在 name 属性可能为 null 值时实例化 embeddedEntity。

如果您需要在一个实体中多次使用某个值对象,可以通过 @Embedded 注解的可选 prefix 元素来实现。该元素表示一个前缀,会被加到内嵌对象中每个列名之前。

提示

使用 @Embedded.Nullable 和 @Embedded.Empty 这两个快捷方式来分别替代 @Embedded(onEmpty = USE_NULL) 和 @Embedded(onEmpty = USE_EMPTY),以减少冗余,同时相应地设置 JSR-305 的 @javax.annotation.Nonnull。

class MyEntity {

@Id
Integer id;

@Embedded.Nullable 1
EmbeddedEntity embeddedEntity;
}
java
  • @Embedded(onEmpty = USE_NULL) 的快捷方式。

包含 Collection 或 Map 的内嵌实体始终被视为非空,因为它们至少会包含空集合或空映射。因此,即使使用 @Embedded(onEmpty = USE_NULL),此类实体也绝不会为 null。

内嵌 ID​

标识符属性可以用 @Embedded 注解,从而允许使用复合 id。整个内嵌实体被视为 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) {
}
java
CREATE TABLE PERSON_WITH_COMPOSITE_ID (
FIRST VARCHAR(100),
LAST VARCHAR(100),
NICK_NAME VARCHAR(100),
AGE INT,
PRIMARY KEY (FIRST, LAST) 3
);
sql
  • 实体可以表示为 record,而无需任何特殊处理。

  • pk 被标记为 id 且为内嵌的。

  • 来自内嵌 Name 实体的两列构成数据库中的主键。

表创建的细节取决于所使用的数据库。

只读属性​

用 @ReadOnlyProperty 注解的属性不会由 Spring Data 写入数据库,但在加载实体时会被读取。

Spring Data 在写入实体后不会自动重新加载它。因此,如果您想查看数据库为此类列生成的数据,必须显式地重新加载它。

如果被注解的属性是一个实体或实体集合,则它由不同表中的一行或多行表示。Spring Data 不会对这些行执行任何插入、删除或更新。

仅插入属性​

用 @InsertOnlyProperty 注解的属性只会由 Spring Data 在插入操作期间写入数据库。对于更新,这些属性将被忽略。

@InsertOnlyProperty 仅支持用于聚合根。

自定义对象构造​

映射子系统允许通过用 @PersistenceConstructor 注解标注构造函数来自定义对象构造。用于构造函数参数的值按如下方式解析:

  • 如果某个参数用 @Value 注解标注,则会计算给定表达式,并将结果用作参数值。

  • 如果 Java 类型有一个属性,其名称与输入行的给定字段匹配,则会使用其属性信息来选择相应的构造函数参数,以将输入字段值传递给它。这仅在 Java .class 文件中存在参数名信息时才有效,您可以通过带调试信息编译源码、或在 Java 8 中对 javac 使用 -parameters 命令行开关来实现。

  • 否则,会抛出 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
}
java

使用显式转换器覆盖映射​

在存储和查询您的对象时,拥有一个 R2dbcConverter 实例来处理所有 Java 类型到 OutboundRow 实例的映射通常会很方便。不过,有时您可能希望 R2dbcConverter 实例完成大部分工作,但让您选择性地处理特定类型的转换——也许是为了优化性能。

要由您自己选择性地处理转换,请向 R2dbcConverter 注册一个或多个 org.springframework.core.convert.converter.Converter 实例。

您可以使用 AbstractR2dbcConfiguration 中的 getCustomConverters 方法来配置转换器。本章开头的示例展示了如何使用 Java 执行配置。

备注

自定义顶层实体转换需要使用不对称的类型进行转换。入站数据从 R2DBC 的 Row 中提取。出站数据(用于 INSERT/UPDATE 语句)表示为 OutboundRow,之后再组装成语句。

以下示例展示了一个 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;
}
}
java

请注意,转换器应用于单个属性。集合属性(例如 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;
}
}
java

使用显式转换器覆盖枚举映射​

某些数据库(例如 Postgres)可以使用其数据库特定的枚举列类型原生地写入枚举值。为获得最大可移植性,Spring Data 默认将 Enum 值转换为 String 值。要保留实际的枚举值,请注册一个 @WritingConverter,其源类型和目标类型都使用实际的枚举类型,以避免使用 Enum.name() 转换。此外,您还需要在驱动程序层面配置枚举类型,以便驱动程序知道如何表示该枚举类型。

以下示例展示了原生读写 Color 枚举值所涉及的各个组件:

enum Color {
Grey, Blue
}

class ColorConverter extends EnumWriteSupport<Color> {

}

class Product {
@Id long id;
Color color;

// …
}
java