映射

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

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

在继续阅读本章之前,请先阅读关于 对象映射基础 的基本内容。

基于约定的映射

当未提供额外的映射元数据时,MappingR2dbcConverter 有一些将对象映射到行的约定。这些约定是

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

  • 不支持嵌套对象。

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

  • 对象的字段用于在行中的列之间进行转换。不使用公共的 JavaBean 属性。

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

映射配置

默认情况下(除非明确配置),当您创建 DatabaseClient 时会创建一个 MappingR2dbcConverter 实例。您可以创建自己的 MappingR2dbcConverter 实例。通过创建自己的实例,您可以注册 Spring converter 来将特定类映射到数据库以及从数据库映射出来。

您可以使用基于 Java 的元数据配置 MappingR2dbcConverterDatabaseClientConnectionFactory。以下示例使用了 Spring 的基于 Java 的配置

如果您将 R2dbcMappingContextsetForceQuote 设置为 true,则从类和属性派生的表名和列名将使用数据库特定的引号。这意味着可以在这些名称中使用保留的 SQL 词(例如 order)。您可以通过覆盖 AbstractR2dbcConfigurationr2dbcMappingContext(Optional<NamingStrategy>) 来实现这一点。Spring Data 会将此类名称的字母大小写转换为配置的数据库在不使用引号时也使用的形式。因此,您可以在创建表时使用不带引号的名称,只要您不在名称中使用关键字或特殊字符。对于符合 SQL 标准的数据库,这意味着名称会转换为大写。引号字符和名称大小写转换的方式由使用的 Dialect 控制。有关如何配置自定义方言,请参阅R2DBC 驱动程序

用于配置 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 的方法。

您可以通过覆盖 r2dbcCustomConversions 方法向 converter 添加额外的 converter。

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

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

基于元数据的映射

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

示例领域对象
package com.mycompany.domain;

@Table
public class Person {

  @Id
  private Long id;

  private Integer ssn;

  private String firstName;

  private String lastName;
}
@Id 注解告诉 mapper 您希望将哪个属性用作主键。

默认类型映射

下表解释了实体的属性类型如何影响映射

源类型 目标类型 备注

基本类型和包装类型

直通

可以使用显式转换器进行自定义。

JSR-310 日期/时间类型

直通

可以使用显式转换器进行自定义。

String, BigInteger, BigDecimal, 和 UUID

直通

可以使用显式转换器进行自定义。

枚举

String

可以通过注册显式转换器进行自定义。

BlobClob

直通

可以使用显式转换器进行自定义。

byte[], ByteBuffer

直通

视为二进制负载。

Collection<T>

T 类型的数组

如果配置的驱动程序支持,则转换为 Array 类型,否则不支持。

基本类型、包装类型和 String 的数组

包装类型的数组(例如 int[]Integer[]

如果配置的驱动程序支持,则转换为 Array 类型,否则不支持。

驱动程序特定类型

直通

由使用的 R2dbcDialect 作为简单类型贡献。

复杂对象

目标类型取决于已注册的 Converter

需要显式转换器,否则不支持。

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

映射注解概述

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

  • @Id: 应用于字段级别,标记主键。

  • @Table: 应用于类级别,表示此类是映射到数据库的候选。您可以指定数据存储在哪个表中。

  • @Transient: 默认情况下,所有字段都映射到行。此注解会排除其应用的字段存储到数据库中。瞬态属性不能在持久化构造函数中使用,因为 converter 无法为构造函数参数提供值。

  • @PersistenceCreator: 标记一个给定的构造函数或静态工厂方法(甚至包括包私有的)在从数据库实例化对象时使用。构造函数参数按名称映射到检索到的行中的值。

  • @Value: 此注解是 Spring Framework 的一部分。在映射框架中,它可以应用于构造函数参数。这允许您使用 Spring Expression Language 语句来转换数据库中检索到的键值,然后再用于构造领域对象。为了引用给定行的列,必须使用如下表达式:@Value("#root.myProperty"),其中 root 指的是给定 Row 的根。

  • @Column: 应用于字段级别,描述列在行中的名称,允许名称与类的字段名不同。使用 @Column 注解指定的名称在 SQL 语句中使用时总是带引号。对于大多数数据库来说,这意味着这些名称是区分大小写的。这也意味着您可以在这些名称中使用特殊字符。然而,不建议这样做,因为它可能会导致与其他工具出现问题。

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

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

命名策略

按照约定,Spring Data 应用 NamingStrategy 来确定表名、列名和 schema 名,默认采用蛇形命名法 (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 支持来动态创建列名。名称一旦生成就会被缓存,因此它只在每个映射上下文内是动态的。

只读属性

@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 方法来配置 converter。本章开头的示例展示了如何使用 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;
  }
}

请注意,converter 应用于单个属性。集合属性(例如 Collection<Person>)会被迭代并按元素进行转换。不支持集合 converter(例如 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 converter,其源类型和目标类型使用实际的枚举类型,以避免使用 Enum.name() 转换。此外,您需要在驱动程序级别配置枚举类型,以便驱动程序知道如何表示枚举类型。

以下示例展示了原生读取和写入 Color 枚举值所涉及的组件

enum Color {
    Grey, Blue
}

class ColorConverter extends EnumWriteSupport<Color> {

}


class Product {
    @Id long id;
    Color color;

    // …
}