映射
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 标准的数据库,这意味着名称会被转换为大写。引号字符和名称大小写的方式由所使用的 Dialect
控制。有关如何配置自定义方言,请参阅 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
方法来为转换器添加额外的转换器。
你可以通过将自定义的 NamingStrategy
注册为一个 bean 来进行配置。NamingStrategy
控制着类和属性的名称如何转换为表和列的名称。
AbstractR2dbcConfiguration
创建一个 DatabaseClient
实例,并将其以 databaseClient
的名称注册到容器中。
基于元数据的映射
为了充分利用 Spring Data R2DBC 支持中的对象映射功能,你应该使用 @Table
注解来标注你的映射对象。虽然映射框架并不一定需要这个注解(即使没有任何注解,你的 POJO 也能正确映射),但它可以让类路径扫描器找到并预处理你的领域对象,以提取必要的元数据。如果你不使用这个注解,应用程序在首次存储领域对象时会有轻微的性能损失,因为映射框架需要构建其内部的元数据模型,以便了解领域对象的属性以及如何持久化它们。以下示例展示了一个领域对象:
package com.mycompany.domain;
@Table
public class Person {
@Id
private Long id;
private Integer ssn;
private String firstName;
private String lastName;
}
@Id
注解告诉映射器你想要使用哪个属性作为主键。
默认类型映射
下表解释了实体属性类型如何影响映射:
源类型 | 目标类型 | 备注 |
---|---|---|
基本类型及其包装类型 | 透传 | 可以使用显式转换器进行自定义。 |
JSR-310 日期/时间类型 | 透传 | 可以使用显式转换器进行自定义。 |
String 、BigInteger 、BigDecimal 和 UUID | 透传 | 可以使用显式转换器进行自定义。 |
Enum | String | 可以通过注册显式转换器进行自定义。 |
Blob 和 Clob | 透传 | 可以使用显式转换器进行自定义。 |
byte[] 、ByteBuffer | 透传 | 视为二进制载荷。 |
Collection<T> | T 的数组 | 如果配置的驱动程序支持,则转换为数组类型,否则不支持。 |
基本类型、包装类型和 String 的数组 | 包装类型的数组(例如 int[] → Integer[] ) | 如果配置的驱动程序支持,则转换为数组类型,否则不支持。 |
驱动程序特定的类型 | 透传 | 由使用的 R2dbcDialect 作为简单类型提供。 |
复杂对象 | 目标类型取决于注册的 Converter 。 | 需要显式转换器,否则不支持。 |
列的原生数据类型取决于 R2DBC 驱动程序的类型映射。驱动程序可以支持额外的简单类型,例如几何类型。
映射注解概述
RelationalConverter
可以使用元数据来驱动对象到行的映射。以下注解可用:
-
@Id
:应用于字段级别,用于标记主键。 -
@Table
:应用于类级别,表示该类是映射到数据库的候选类。你可以指定存储数据库的表的名称。 -
@Transient
:默认情况下,所有字段都映射到行。此注解将应用它的字段排除在数据库存储之外。瞬态属性不能在持久化构造函数中使用,因为转换器无法为构造函数参数实例化值。 -
@PersistenceCreator
:标记一个给定的构造函数或静态工厂方法——即使是包保护的——在从数据库实例化对象时使用。构造函数参数通过名称映射到检索行中的值。 -
@Value
:此注解是 Spring 框架的一部分。在映射框架中,它可以应用于构造函数参数。这允许你在使用数据库检索的键值来构造域对象之前,使用 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 支持 来动态创建列名。一旦生成,这些名称将被缓存,因此仅在每个映射上下文中是动态的。
只读属性
使用 @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;
// …
}