映射

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

本节介绍 MappingR2dbcConverter 的功能,包括如何使用约定将对象映射到行,以及如何通过基于注解的映射元数据来覆盖这些约定。spring-doc.cadn.net.cn

在继续阅读本章之前,请先了解对象映射基础的相关内容。spring-doc.cadn.net.cn

基于约定的映射

MappingR2dbcConverter 在未提供额外映射元数据时,有一些将对象映射到行的约定。 这些约定如下:spring-doc.cadn.net.cn

  • 短 Java 类名按以下方式映射到表名。 com.bigbank.SavingsAccount 类映射为 SAVINGS_ACCOUNT 表名。 同样的名称映射规则也适用于将字段映射到列名。 例如,firstName 字段映射为 FIRST_NAME 列。 你可以通过提供自定义的 NamingStrategy 来控制此映射行为。 更多详细信息,请参见映射配置。 默认情况下,从属性名或类名派生出的表名和列名在 SQL 语句中不使用引号。 你可以通过设置 RelationalMappingContext.setForceQuote(true) 来控制这一行为。spring-doc.cadn.net.cn

  • 不支持嵌套对象。spring-doc.cadn.net.cn

  • 该转换器使用通过 CustomConversions 注册的任何 Spring 转换器,以覆盖对象属性到行的列和值的默认映射。spring-doc.cadn.net.cn

  • 对象的字段用于在行中的列之间进行转换。 不会使用公共的 JavaBean 属性。spring-doc.cadn.net.cn

  • 如果你有一个唯一的非零参数构造函数,且其构造函数参数名称与行的顶层列名匹配,则使用该构造函数。 否则,将使用无参构造函数。 如果存在多个非零参数构造函数,则会抛出异常。 更多详细信息,请参阅对象创建spring-doc.cadn.net.cn

映射配置

默认情况下(除非显式配置),在创建 MappingR2dbcConverter 时会自动创建一个 DatabaseClient 实例。 您可以创建自己的 MappingR2dbcConverter 实例。 通过创建自己的实例,您可以注册 Spring 转换器,用于将特定类与数据库之间进行映射。spring-doc.cadn.net.cn

你可以使用基于 Java 的元数据来配置 MappingR2dbcConverterDatabaseClient 以及 ConnectionFactory。 以下示例使用了 Spring 的基于 Java 的配置:spring-doc.cadn.net.cn

如果将 setForceQuoteR2dbcMappingContext to 设置为 true,则从类和属性派生出的表名和列名将使用数据库特定的引号。 这意味着在这些名称中使用 SQL 保留字(例如 order)是安全的。 您可以通过重写 r2dbcMappingContext(Optional<NamingStrategy>) 中的 AbstractR2dbcConfiguration 方法来实现这一点。 当未使用引号时,Spring Data 会将此类名称的字母大小写转换为所配置数据库通常使用的格式。 因此,只要您的名称中不包含关键字或特殊字符,创建表时就可以使用不带引号的名称。 对于遵循 SQL 标准的数据库,这意味着名称会被转换为大写形式。 引号字符以及名称的大写方式由所使用的 R2dbcDialect 控制。 有关如何配置自定义方言,请参见R2DBC 驱动程序spring-doc.cadn.net.cn

用于配置 R2DBC 映射支持的 @Configuration 类
@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 的方法。spring-doc.cadn.net.cn

你可以通过重写 r2dbcCustomConversions 方法向转换器中添加额外的转换器。spring-doc.cadn.net.cn

你可以通过将其注册为一个 Bean 来配置自定义的 NamingStrategyNamingStrategy 控制类名和属性名如何转换为表名和列名。spring-doc.cadn.net.cn

AbstractR2dbcConfiguration 创建一个 DatabaseClient 实例,并将其以 databaseClient 的名称注册到容器中。

基于元数据的映射

为了充分利用 Spring Data R2DBC 支持中的对象映射功能,您应该使用 @Table 注解来标注您的映射对象。 尽管映射框架并不强制要求此注解(即使没有任何注解,您的 POJO 也能被正确映射),但它能让类路径扫描器发现并预处理您的领域对象,以提取必要的元数据。 如果您不使用此注解,那么在首次存储领域对象时,应用程序会受到轻微的性能影响,因为映射框架需要构建其内部元数据模型,以便了解您的领域对象的属性以及如何持久化它们。 以下示例展示了一个领域对象:spring-doc.cadn.net.cn

示例领域对象
@Table
public class Person {

    @Id
    private Long id;

    private Integer ssn;

    private String firstName;

    private String lastName;
}
@Id 注解告诉映射器你想将哪个属性用作主键。

默认类型映射

下表说明了实体的属性类型如何影响映射:spring-doc.cadn.net.cn

源类型 目标类型 备注

基本类型和包装类型spring-doc.cadn.net.cn

透传spring-doc.cadn.net.cn

可以使用显式转换器进行自定义。spring-doc.cadn.net.cn

JSR-310 日期/时间类型spring-doc.cadn.net.cn

透传spring-doc.cadn.net.cn

可以使用显式转换器进行自定义。spring-doc.cadn.net.cn

StringBigIntegerBigDecimalUUIDspring-doc.cadn.net.cn

透传spring-doc.cadn.net.cn

可以使用显式转换器进行自定义。spring-doc.cadn.net.cn

Enumspring-doc.cadn.net.cn

字符串spring-doc.cadn.net.cn

可通过注册显式转换器进行自定义。spring-doc.cadn.net.cn

BlobClobspring-doc.cadn.net.cn

透传spring-doc.cadn.net.cn

可以使用显式转换器进行自定义。spring-doc.cadn.net.cn

byte[], ByteBufferspring-doc.cadn.net.cn

透传spring-doc.cadn.net.cn

被视为二进制有效载荷。spring-doc.cadn.net.cn

Collection<T>spring-doc.cadn.net.cn

T 的数组spring-doc.cadn.net.cn

如果配置的驱动程序支持,则转换为数组类型;否则不支持。spring-doc.cadn.net.cn

基本类型数组、包装类型数组和Stringspring-doc.cadn.net.cn

包装类型数组(例如 int[]Integer[]spring-doc.cadn.net.cn

如果配置的驱动程序支持,则转换为数组类型;否则不支持。spring-doc.cadn.net.cn

特定于驱动程序的类型spring-doc.cadn.net.cn

透传spring-doc.cadn.net.cn

由所使用的 R2dbcDialect 作为简单类型提供。spring-doc.cadn.net.cn

复杂对象spring-doc.cadn.net.cn

目标类型取决于已注册的Converterspring-doc.cadn.net.cn

需要一个显式转换器,否则不支持。spring-doc.cadn.net.cn

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

映射注解概述

RelationalConverter 可以使用元数据来驱动对象到行的映射。 以下注解可用:spring-doc.cadn.net.cn

  • @Embedded:带有此注解的属性将映射到父实体的表中,而不是单独的表。 允许指定生成的列是否应具有一个公共前缀。 如果此类实体生成的所有列均为 null,则根据 null 的值,被注解的实体将为 nullempty(即其所有属性均为 @Embedded.onEmpty())。 可与 @Id 结合使用以构成复合主键。spring-doc.cadn.net.cn

  • @Id:应用于字段级别,用于标记主键。 它可以与 @Embedded 结合使用,以构成复合主键。spring-doc.cadn.net.cn

  • @InsertOnlyProperty:将一个属性标记为仅在插入时写入。 聚合根上的此类属性仅会被写入一次,之后永远不会更新。 请注意,在嵌套实体上,所有保存操作都会导致插入操作,因此该注解对嵌套实体的属性无效。spring-doc.cadn.net.cn

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

  • @Sequence:为被注解的属性指定一个数据库序列,用于生成值。spring-doc.cadn.net.cn

  • @Table:在类级别上使用,用于表明该类是映射到数据库的候选对象。 您可以指定存储该数据库表的名称。spring-doc.cadn.net.cn

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

  • @PersistenceCreator:标记一个给定的构造函数或静态工厂方法(即使是包级私有的)在从数据库实例化对象时使用。 构造函数参数通过名称映射到所检索行中的值。spring-doc.cadn.net.cn

  • @Value:此注解是 Spring 框架的一部分。 在映射框架中,它可以应用于构造函数参数。 这允许你使用 Spring 表达式语言(SpEL)语句,在从数据库检索到某个键的值之后、用于构造领域对象之前对其进行转换。 为了引用给定行中的某一列,必须使用类似如下的表达式:@Value("#root.myProperty"),其中 root 指向给定 Row 的根。spring-doc.cadn.net.cn

  • @Column:应用于字段级别,用于描述该字段在数据库行中所对应的列名,允许列名与类中的字段名不同。 使用 @Column 注解指定的名称在 SQL 语句中始终会被加上引号。 对于大多数数据库而言,这意味着这些名称是区分大小写的。 这也意味着你可以在这些名称中使用特殊字符。 然而,我们不建议这样做,因为这可能会与其他工具产生兼容性问题。spring-doc.cadn.net.cn

  • @Version:应用于字段级别,用于乐观锁,并在保存操作时检查是否发生修改。 值为 null(对于基本类型为 zero)被视为新实体的标记。 初始存储的值为 zero(对于基本类型为 one)。 版本号会在每次更新时自动递增。 更多详情请参阅乐观锁spring-doc.cadn.net.cn

映射元数据基础设施定义在独立的、与技术无关的 spring-data-commons 项目中。 R2DBC 支持中使用了特定的子类来支持基于注解的元数据。 也可以根据需求引入其他策略。spring-doc.cadn.net.cn

命名策略

按照约定,Spring Data 应用 NamingStrategy 来确定表、列和模式名称,默认采用 蛇形命名法(snake case)。 名为 firstName 的对象属性将变为 first_name。 您可以在应用程序上下文中提供 NamingStrategy 来调整这一行为。spring-doc.cadn.net.cn

覆盖表名

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

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

    String name;
}

您可以使用Spring Data 的 SpEL 支持来动态创建表名。 一旦生成,表名将被缓存,因此它仅在每个映射上下文中是动态的。spring-doc.cadn.net.cn

覆盖列名

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

class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    String name;
}

您可以使用Spring Data 的 SpEL 支持来动态创建列名。 一旦生成,这些名称将被缓存,因此仅在每个映射上下文中具有动态性。spring-doc.cadn.net.cn

嵌入的 ID

标识符属性可以使用 @Embedded 注解,从而允许使用复合主键(composite ids)。 整个嵌入的实体被视为 ID,因此在判断一个聚合是新聚合(需要执行插入操作)还是已有聚合(需要执行更新操作)时,依据的是该实体本身,而不是其内部的各个元素。 大多数使用场景都需要自定义一个 BeforeConvertCallback,以便为新的聚合设置 ID。spring-doc.cadn.net.cn

具有复合主键的简单实体
@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实体中的两个列构成了数据库中的主键。

表的创建细节取决于所使用的数据库。spring-doc.cadn.net.cn

只读属性

使用 @ReadOnlyProperty 注解的属性不会被 Spring Data 写入数据库,但在加载实体时会被读取。spring-doc.cadn.net.cn

Spring Data 在写入实体后不会自动重新加载该实体。 因此,如果你希望看到数据库为此类列生成的数据,必须显式地重新加载它。spring-doc.cadn.net.cn

如果被注解的属性是一个实体或实体集合,则它在单独的表中由一行或多行表示。 Spring Data 不会对这些行执行任何插入、删除或更新操作。spring-doc.cadn.net.cn

仅插入属性

使用 @InsertOnlyProperty 注解的属性仅在插入操作期间由 Spring Data 写入数据库。 在更新操作中,这些属性将被忽略。spring-doc.cadn.net.cn

@InsertOnlyProperty 仅支持聚合根。spring-doc.cadn.net.cn

自定义对象构建

映射子系统允许通过使用 @PersistenceConstructor 注解标注构造函数来定制对象的构造方式。构造函数参数所使用的值将按以下方式解析:spring-doc.cadn.net.cn

  • 如果一个参数使用 @Value 注解进行标注,则会计算给定的表达式,并将结果用作该参数的值。spring-doc.cadn.net.cn

  • 如果 Java 类型具有一个属性,其名称与输入行中的给定字段相匹配,那么将使用该属性的信息来选择合适的构造函数参数,以传递输入字段的值。 这仅在 Java .class 文件中包含参数名称信息时才有效,您可以通过使用调试信息编译源代码,或在 Java 8 中使用 -parametersjavac 命令行选项来实现这一点。spring-doc.cadn.net.cn

  • 否则,将抛出一个 MappingException,以表明给定的构造函数参数无法被绑定。spring-doc.cadn.net.cn

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 实例完成大部分工作,但又能让你针对特定类型有选择性地处理转换——例如为了优化性能。spring-doc.cadn.net.cn

要选择性地自行处理转换,请向 org.springframework.core.convert.converter.Converter 注册一个或多个 R2dbcConverter 实例。spring-doc.cadn.net.cn

您可以使用 r2dbcCustomConversions 中的 AbstractR2dbcConfiguration 方法来配置转换器。 本章开头的示例展示了如何使用 Java 进行配置。spring-doc.cadn.net.cn

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

以下是一个 Spring Converter 实现的示例,它将 Row 转换为 Person POJO:spring-doc.cadn.net.cn

@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)。spring-doc.cadn.net.cn

R2DBC 使用包装类型(Integer.class 而不是 int.class)来返回基本类型的值。

以下示例将 Person 转换为 OutboundRowspring-doc.cadn.net.cn

@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() 进行转换。 此外,您还需要在驱动程序级别配置枚举类型,以便驱动程序知道如何表示该枚举类型。spring-doc.cadn.net.cn

以下示例展示了用于原生读取和写入 Color 枚举值所涉及的组件:spring-doc.cadn.net.cn

enum Color {
    Grey, Blue
}

class ColorConverter extends EnumWriteSupport<Color> {

}


class Product {
    @Id long id;
    Color color;

    // …
}