映射

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 方法向转换器添加额外的转换器。

您可以通过将自定义 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 日期/时间类型

直通

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

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;
}

覆盖列名

当列命名策略与您的数据库表名不匹配时,您可以使用 @Column 注释覆盖表名。此注释的元素 value 提供自定义列名。以下示例将 MyEntity 类的 name 属性映射到数据库中的 CUSTOM_COLUMN_NAME

class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    String 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;

    // …
}