映射
MappingR2dbcConverter 提供了丰富的映射支持。MappingR2dbcConverter 具有丰富的元数据模型,允许将领域对象映射到数据行。映射元数据模型通过在领域对象上使用注解进行填充。然而,基础设施并不限于仅使用注解作为元数据信息的来源。MappingR2dbcConverter 还允许您通过遵循一组约定,在不提供任何额外元数据的情况下将对象映射到行。
本节描述了 MappingR2dbcConverter 的功能,包括如何使用约定将对象映射到行,以及如何使用基于注解的映射元数据覆盖这些约定。
在继续本章之前,请阅读有关对象映射基础知识。
基于约定的映射
当未提供额外映射元数据时,MappingR2dbcConverter 有一些将对象映射到行的约定。这些约定是
-
Java 类的短名称以以下方式映射到表名。
com.bigbank.SavingsAccount类映射到SAVINGS_ACCOUNT表名。字段到列名的映射也应用相同的名称映射。例如,firstName字段映射到FIRST_NAME列。您可以通过提供自定义NamingStrategy来控制此映射。有关更多详细信息,请参阅映射配置。默认情况下,从属性或类名派生的表名和列名在 SQL 语句中不带引号使用。您可以通过设置RelationalMappingContext.setForceQuote(true)来控制此行为。 -
不支持嵌套对象。
-
转换器使用注册到
CustomConversions的任何 Spring 转换器来覆盖对象属性到行、列和值的默认映射。 -
对象的字段用于在行中转换为列和从列转换。不使用公共
JavaBean属性。 -
如果您有一个唯一的非零参数构造函数,其构造函数参数名称与行的顶级列名称匹配,则使用该构造函数。否则,使用零参数构造函数。如果有多个非零参数构造函数,则抛出异常。有关更多详细信息,请参阅对象创建。
映射配置
默认情况下(除非明确配置),当您创建 DatabaseClient 时,会创建一个 MappingR2dbcConverter 实例。您可以创建自己的 MappingR2dbcConverter 实例。通过创建自己的实例,您可以注册 Spring 转换器,以将特定类映射到数据库以及从数据库映射。
您可以使用基于 Java 的元数据配置 MappingR2dbcConverter 以及 DatabaseClient 和 ConnectionFactory。以下示例使用 Spring 的基于 Java 的配置
如果您将 R2dbcMappingContext 的 setForceQuote 设置为 true,则从类和属性派生的表名和列名将与数据库特定的引号一起使用。这意味着可以使用保留的 SQL 关键字(例如 order)作为这些名称。您可以通过覆盖 AbstractR2dbcConfiguration 的 r2dbcMappingContext(Optional<NamingStrategy>) 来实现。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 驱动程序类型映射。驱动程序可以贡献额外的简单类型,例如几何类型。 |
映射注解概述
RelationalConverter 可以使用元数据来驱动对象到行的映射。以下注解可用
-
@Embedded:带有此注解的属性将映射到父实体的表,而不是单独的表。允许指定生成的列是否应具有公共前缀。如果由此实体生成的所有列都为null,则注解的实体将为null或 空,即其所有属性都将为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:应用于字段级别,描述列在行中表示的名称,允许名称与类的字段名称不同。使用@Column注解指定的名称在 SQL 语句中始终带引号。对于大多数数据库,这意味着这些名称区分大小写。这也意味着您可以在这些名称中使用特殊字符。但是,不建议这样做,因为它可能会导致其他工具出现问题。 -
@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;
}
您可以使用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 注解,允许使用复合 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) {
}
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 中使用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
}
使用显式转换器覆盖映射
在存储和查询对象时,拥有一个 R2dbcConverter 实例来处理所有 Java 类型到 OutboundRow 实例的映射通常很方便。但是,您有时可能希望 R2dbcConverter 实例完成大部分工作,但让您选择性地处理特定类型的转换——也许是为了优化性能。
要选择性地自行处理转换,请向 R2dbcConverter 注册一个或多个 org.springframework.core.convert.converter.Converter 实例。
您可以在 AbstractR2dbcConfiguration 中使用 r2dbcCustomConversions 方法来配置转换器。本章开头的示例展示了如何使用 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;
}
}
请注意,转换器应用于单个属性。集合属性(例如 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;
}
}
使用显式转换器覆盖枚举映射
某些数据库,例如 Postgres,可以使用其特定于数据库的枚举列类型本地写入枚举值。Spring Data 默认将 Enum 值转换为 String 值以实现最大可移植性。为了保留实际的枚举值,请注册一个 @Writing 转换器,其源类型和目标类型使用实际的枚举类型,以避免使用 Enum.name() 转换。此外,您需要配置驱动程序级别的枚举类型,以便驱动程序知道如何表示枚举类型。
以下示例显示了本地读写 Color 枚举值所涉及的组件
enum Color {
Grey, Blue
}
class ColorConverter extends EnumWriteSupport<Color> {
}
class Product {
@Id long id;
Color color;
// …
}