映射

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 以及 DatabaseClientConnectionFactory。以下示例使用 Spring 的基于 Java 的配置

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

@Configuration 类用于配置 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 来配置自定义 NamingStrategyNamingStrategy 控制类和属性的名称如何转换为表和列的名称。

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 日期/时间类型

直通

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

StringBigIntegerBigDecimalUUID

直通

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

枚举

String

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

BlobClob

直通

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

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 实例执行大部分工作,但让您可以选择性地处理特定类型的转换——也许是为了优化性能。

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

您可以在 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;

    // …
}