映射

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

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

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

基于约定的映射

当未提供额外映射元数据时,MappingJdbcConverter 有一些约定用于将对象映射到行。这些约定包括:

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

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

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

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

实体中支持的类型

目前支持以下类型的属性:

  • 所有基本类型及其包装类型(如 int, float, Integer, Float 等)

  • 枚举类型映射到其名称。

  • String

  • java.util.Date, java.time.LocalDate, java.time.LocalDateTime, 和 java.time.LocalTime

  • 上述类型的数组和集合可以映射到数组类型的列,如果您的数据库支持的话。

  • 您的数据库驱动程序接受的任何类型。

  • 对其他实体的引用。它们被视为一对一关系或嵌入类型。一对一关系实体可选包含 id 属性。被引用实体的表预计会有一个额外的列,其名称基于引用实体(见反向引用)。嵌入实体不需要 id。如果存在 id,它会被映射为普通属性,没有特殊含义。

  • Set<some entity> 被视为一对多关系。被引用实体的表预计会有一个额外的列,其名称基于引用实体(见反向引用)。

  • Map<simple type, some entity> 被视为限定一对多关系。被引用实体的表预计会有两个额外的列:一个基于引用实体作为外键的列(见反向引用),以及一个与外键列同名并附加 _key 后缀用于映射键的列。

  • List<some entity> 被映射为 Map<Integer, some entity>。预计会有相同的额外列,并且使用的名称可以通过相同的方式定制。

    对于 ListSetMap,反向引用的命名可以通过实现 NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner)NamingStrategy.getKeyColumn(RelationalPersistentProperty property) 来控制。或者,您可以使用 @MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name") 注解属性。为 Set 指定键列没有效果。

  • 您为其注册了合适的自定义转换器的类型。

映射注解概览

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

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

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

  • @Transient:默认情况下,所有字段都映射到行。此注解将应用的字段从数据库存储中排除。Transient 属性不能用于持久化构造函数中,因为转换器无法为构造函数参数实例化值。

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

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

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

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

更多详情请参阅乐观锁

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

引用实体

引用实体的处理是有限的。这是基于上述聚合根的理念。如果您引用另一个实体,该实体根据定义是您的聚合的一部分。因此,如果您移除引用,之前引用的实体将被删除。这也意味着引用是 1 对 1 或 1 对多,而不是多对 1 或多对多。

如果您有多对 1 或多对多引用,根据定义,您正在处理两个独立的聚合。这些之间的引用可以编码为简单的 id 值,这些值可以与 Spring Data JDBC 正确映射。一种更好的编码方式是将它们设为 AggregateReference 的实例。AggregateReference 是一个围绕 id 值的包装器,它将该值标记为对不同聚合的引用。此外,该聚合的类型被编码在类型参数中。

反向引用

聚合中的所有引用都会在数据库中生成一个反向方向的外键关系。默认情况下,外键列的名称是引用实体的表名。

或者,您可以选择根据引用实体的实体名称来命名它们,忽略 @Table 注解。您可以通过在 RelationalMappingContext 上调用 setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING) 来激活此行为。

对于 ListMap 引用,需要一个额外的列来保存列表索引或映射键。该列基于外键列,并附加 _KEY 后缀。

如果您想要一种完全不同的方式来命名这些反向引用,可以实现 NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner) 以满足您的需求。

声明和设置 AggregateReference
class Person {
	@Id long id;
	AggregateReference<Person, Long> bestFriend;
}

// ...

Person p1, p2 = // some initialization

p1.bestFriend = AggregateReference.to(p2.id);

您不应该在实体中包含属性来保存反向引用的实际值,也不应保存映射或列表的键列值。如果您希望这些值在您的域模型中可用,我们建议在 AfterConvertCallback 中完成,并将值存储在 transient 值中。

命名策略

按照约定,Spring Data 应用 NamingStrategy 来确定表名、列名和 schema 名称,默认为蛇形命名法。一个名为 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;
}

MappedCollection 注解可用于引用类型(一对一关系)或 Sets、Lists 和 Maps(一对多关系)。注解的 idColumn 元素提供了引用另一个表中 id 列的外键列的自定义名称。在以下示例中,MySubEntity 类对应的表有一个 NAME 列,以及由于关系原因,MyEntity id 的 CUSTOM_MY_ENTITY_ID_COLUMN_NAME

class MyEntity {
    @Id
    Integer id;

    @MappedCollection(idColumn = "CUSTOM_MY_ENTITY_ID_COLUMN_NAME")
    Set<MySubEntity> subEntities;
}

class MySubEntity {
    String name;
}

使用 ListMap 时,必须有一个额外的列来存储数据集中在 List 中的位置或实体在 Map 中的键值。此额外列名可以使用MappedCollection 注解的 keyColumn 元素进行定制

class MyEntity {
    @Id
    Integer id;

    @MappedCollection(idColumn = "CUSTOM_COLUMN_NAME", keyColumn = "CUSTOM_KEY_COLUMN_NAME")
    List<MySubEntity> name;
}

class MySubEntity {
    String name;
}

您可以使用Spring Data 的 SpEL 支持来动态创建列名。一旦生成,名称将被缓存,因此它仅在每个映射上下文动态。

嵌入实体

嵌入实体用于在您的 Java 数据模型中包含值对象,即使您的数据库中只有一个表。在以下示例中,您看到 MyEntity 使用 @Embedded 注解进行映射。这样做的结果是,数据库中预计会有一个名为 my_entity 的表,其中包含两个列:idname(来自 EmbeddedEntity 类)。

但是,如果结果集中 name 列实际为 null,则根据 @EmbeddedonEmpty(当所有嵌套属性都为 null 时,将对象设为 null),整个 embeddedEntity 属性将被设为 null。
与此行为相反,USE_EMPTY 尝试使用默认构造函数或接受结果集中可空参数值的构造函数来创建新实例。

示例 1. 嵌入对象的示例代码
class MyEntity {

    @Id
    Integer id;

    @Embedded(onEmpty = USE_NULL) (1)
    EmbeddedEntity embeddedEntity;
}

class EmbeddedEntity {
    String name;
}
1 如果 namenull,则将 embeddedEntity 设为 Null。使用 USE_EMPTY 来实例化 embeddedEntity,并为 name 属性提供潜在的 null 值。

如果您在一个实体中需要多次使用一个值对象,可以通过 @Embedded 注解的可选 prefix 元素来实现。该元素表示一个前缀,并会在嵌入对象中的每个列名前添加。

使用快捷方式 @Embedded.Nullable@Embedded.Empty 代替 @Embedded(onEmpty = USE_NULL)@Embedded(onEmpty = USE_EMPTY),以减少冗余并同时设置 JSR-305 @javax.annotation.Nonnull

class MyEntity {

    @Id
    Integer id;

    @Embedded.Nullable (1)
    EmbeddedEntity embeddedEntity;
}
1 @Embedded(onEmpty = USE_NULL) 的快捷方式。

包含 CollectionMap 的嵌入实体始终被视为非空,因为它们至少会包含空集合或空映射。因此,即使使用 @Embedded(onEmpty = USE_NULL),此类实体也不会为 null

只读属性

使用 @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
}

使用显式转换器覆盖映射

Spring Data 允许注册自定义转换器来影响数据库中值的映射方式。目前,转换器仅应用于属性级别,即您只能将域中的单个值转换为数据库中的单个值并反向转换。不支持复杂对象和多列之间的转换。

使用注册的 Spring Converter 写入属性

以下示例展示了 Converter 的实现,该实现将 Boolean 对象转换为 String

import org.springframework.core.convert.converter.Converter;

@WritingConverter
public class BooleanToStringConverter implements Converter<Boolean, String> {

    @Override
    public String convert(Boolean source) {
        return source != null && source ? "T" : "F";
    }
}

这里有几点需要注意:BooleanString 都是简单类型,因此 Spring Data 需要一个提示来确定此转换器应应用于哪个方向(读取或写入)。通过使用 @WritingConverter 注解此转换器,您指示 Spring Data 将每个 Boolean 属性作为 String 写入数据库。

使用 Spring Converter 读取

以下示例展示了 Converter 的实现,该实现将 String 转换为 Boolean

@ReadingConverter
public class StringToBooleanConverter implements Converter<String, Boolean> {

    @Override
    public Boolean convert(String source) {
        return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE;
    }
}

这里有几点需要注意:StringBoolean 都是简单类型,因此 Spring Data 需要一个提示来确定此转换器应应用于哪个方向(读取或写入)。通过使用 @ReadingConverter 注解此转换器,您指示 Spring Data 转换数据库中应分配给 Boolean 属性的每个 String 值。

将 Spring Converter 注册到 JdbcConverter

class MyJdbcConfiguration extends AbstractJdbcConfiguration {

    // …

    @Override
    protected List<?> userConverters() {
	return Arrays.asList(new BooleanToStringConverter(), new StringToBooleanConverter());
    }

}
在旧版本的 Spring Data JDBC 中,建议直接覆盖 AbstractJdbcConfiguration.jdbcCustomConversions()。这不再是必要或推荐的做法,因为该方法会组装所有数据库的转换、使用的 Dialect 注册的转换以及用户注册的转换。如果您正在从旧版本的 Spring Data JDBC 迁移,并且覆盖了 AbstractJdbcConfiguration.jdbcCustomConversions(),则您的 Dialect 中的转换将不会被注册。

如果您想依赖Spring Boot 来引导 Spring Data JDBC,但仍想覆盖配置的某些方面,您可以选择暴露该类型的 bean。对于自定义转换,您可以例如选择注册一个 JdbcCustomConversions 类型的 bean,该 bean 将由 Boot 基础设施拾取。要了解更多信息,请务必阅读 Spring Boot 参考文档

JdbcValue

值转换使用 JdbcValue 来丰富传播到 JDBC 操作的值,并添加一个 java.sql.Types 类型。如果您需要指定 JDBC 特定的类型而不是使用类型推导,请注册一个自定义写入转换器。此转换器应将值转换为 JdbcValue,该类型包含值字段和实际的 JDBCType 字段。