映射

BasicJdbcConverter 提供了丰富的映射支持。BasicJdbcConverter 拥有一个丰富的元数据模型,允许将域对象映射到数据行。映射元数据模型通过在域对象上使用注解来填充。但是,基础设施并不局限于使用注解作为唯一的元数据信息来源。BasicJdbcConverter 还允许您将对象映射到行,而无需提供任何额外的元数据,方法是遵循一组约定。

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

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

基于约定的映射

当没有提供额外的映射元数据时,BasicJdbcConverter 有一些将对象映射到行的约定。约定如下:

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

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

  • 对象的字段用于转换为行中的列以及从行中的列转换。不使用公共 JavaBean 属性。

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

实体中支持的类型

目前支持以下类型的属性

  • 所有基本类型及其包装类型(intfloatIntegerFloat 等)

  • 枚举映射到其名称。

  • 字符串

  • java.util.Datejava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTime

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

  • 您的数据库驱动程序接受的任何内容。

  • 对其他实体的引用。它们被视为一对一关系或嵌入类型。一对一关系实体可以选择是否具有 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:默认情况下,所有字段都映射到行。此注解将应用它的字段排除在数据库中存储之外。瞬态属性不能在持久化构造函数中使用,因为转换器无法为构造函数参数实现值。

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

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

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

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

有关更多参考,请参见 乐观锁

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

引用实体

引用实体的处理是有限的。这是基于上面描述的聚合根的概念。如果您引用另一个实体,那么该实体根据定义是您聚合的一部分。因此,如果您删除引用,则先前引用的实体将被删除。这也意味着引用是 1-1 或 1-n,但不是 n-1 或 n-m。

如果您有 n-1 或 n-m 引用,那么根据定义,您正在处理两个独立的聚合。这些聚合之间的引用可以编码为简单的 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中执行此操作,并将值存储在瞬态值中。

  • 您为其注册了合适的的类型。

命名策略

按照惯例,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;
}

@MappedCollection 注解可以用于引用类型(一对一关系)或集合类型(一对多关系),例如 Set、List 和 Map。注解的 idColumn 元素为引用另一个表中 id 列的外键列提供自定义名称。在以下示例中,MySubEntity 类的对应表具有 NAME 列,而 MyEntityCUSTOM_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;
}

嵌入实体

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

但是,如果结果集中的 name 列实际上为 null,则根据 @EmbeddedonEmpty 属性,整个 embeddedEntity 属性将被设置为 null,该属性在所有嵌套属性都为 null 时将对象设置为 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 使用 name 属性的潜在 null 值实例化 embeddedEntity

如果您需要在一个实体中多次使用值对象,可以使用 @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 转换器写入属性

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

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 转换器读取

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

@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 值进行转换。

使用 JdbcConverter 注册 Spring 转换器

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的字段。