映射

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

本节介绍 MappingJdbcConverter 的功能,包括如何使用约定将对象映射到行,以及如何通过基于注解的映射元数据来覆盖这些约定。spring-doc.cadn.net.cn

在继续阅读本章之前,请先了解对象映射基础的相关内容。spring-doc.cadn.net.cn

基于约定的映射

MappingJdbcConverter 在未提供额外映射元数据时,有一些将对象映射到行的约定。 这些约定如下:spring-doc.cadn.net.cn

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

  • 该转换器使用通过 CustomConversions 注册的任何 Spring 转换器,以覆盖对象属性到行的列和值的默认映射。spring-doc.cadn.net.cn

  • 对象的字段用于在行中的列之间进行转换。 不会使用公共的 JavaBean 属性。spring-doc.cadn.net.cn

  • 如果你有一个唯一的非零参数构造函数,且其构造函数参数名称与行的顶层列名匹配,则使用该构造函数。 否则,将使用无参构造函数。 如果存在多个非零参数构造函数,则会抛出异常。 更多详细信息,请参阅对象创建spring-doc.cadn.net.cn

实体中支持的类型

目前支持以下类型属性:spring-doc.cadn.net.cn

  • 所有基本类型及其对应的包装类型(intfloatIntegerFloat 等)spring-doc.cadn.net.cn

  • 枚举会被映射为其名称。spring-doc.cadn.net.cn

  • Stringspring-doc.cadn.net.cn

  • java.util.Datejava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTimespring-doc.cadn.net.cn

  • 如果您的数据库支持,上述类型的数组和集合可以映射到数组类型的列。spring-doc.cadn.net.cn

  • 数据库驱动程序所接受的任何内容。spring-doc.cadn.net.cn

  • 对其他实体的引用。 它们被视为一对一关系,或嵌入类型。 对于一对一关系的实体来说,拥有 id 属性是可选的。 被引用实体对应的表应包含一个额外的列,该列的名称基于引用实体,请参见反向引用。 嵌入式实体不需要 id。 如果存在 3,它将被映射为一个普通属性,不具有任何特殊含义。spring-doc.cadn.net.cn

  • Set<some entity> 被视为一对多关系。 被引用实体的表应包含一个额外的列,该列的名称基于引用实体,请参见反向引用spring-doc.cadn.net.cn

  • Map<simple type, some entity> 被视为一种合格的一对多关系。 被引用实体的表应包含两个额外的列:一个基于引用实体命名,用作外键(参见反向引用);另一个名称相同,但附加了 _key 后缀,用于表示 Map 的键。spring-doc.cadn.net.cn

  • List<some entity> 被映射为 Map<Integer, some entity>。 预期会有相同的附加列,并且所使用的名称可以以相同的方式进行自定义。spring-doc.cadn.net.cn

    对于 ListSetMap,反向引用的命名可通过分别实现 NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner)NamingStrategy.getKeyColumn(RelationalPersistentProperty property) 来控制。 或者,您也可以使用 @MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name") 注解该属性。 为 Set 指定键列(key column)不会产生任何效果。spring-doc.cadn.net.cn

  • 已为其注册了合适自定义转换器的类型。spring-doc.cadn.net.cn

映射注解概述

RelationalConverter 可以使用元数据来驱动对象到行的映射。 以下注解可用:spring-doc.cadn.net.cn

  • @Embedded:带有此注解的属性将映射到父实体的表中,而不是单独的表。 允许指定生成的列是否应具有一个公共前缀。 如果此类实体生成的所有列均为 null,则根据 null 的值,被注解的实体将为 nullempty(即其所有属性均为 @Embedded.onEmpty())。 可与 @Id 结合使用以构成复合主键。spring-doc.cadn.net.cn

  • @Id:应用于字段级别,用于标记主键。 它可以与 @Embedded 结合使用,以构成复合主键。spring-doc.cadn.net.cn

  • @InsertOnlyProperty:将一个属性标记为仅在插入时写入。 聚合根上的此类属性仅会被写入一次,之后永远不会更新。 请注意,在嵌套实体上,所有保存操作都会导致插入操作,因此该注解对嵌套实体的属性无效。spring-doc.cadn.net.cn

  • @MappedCollection:允许配置集合或单个嵌套实体的映射方式。idColumn 指定用于引用父实体主键的列。keyColumn 指定用于存储 List 的索引或 Map 的键的列。spring-doc.cadn.net.cn

  • @Sequence:为被注解的属性指定一个数据库序列,用于生成值。spring-doc.cadn.net.cn

  • @Table:在类级别上使用,用于表明该类是映射到数据库的候选对象。 您可以指定存储该数据库表的名称。spring-doc.cadn.net.cn

  • @Transient:默认情况下,所有字段都会映射到数据库行中。 此注解用于将其所标注的字段排除在数据库存储之外。 瞬态(Transient)属性不能在持久化构造函数中使用,因为转换器无法为构造函数参数生成一个值。spring-doc.cadn.net.cn

  • @PersistenceCreator:标记一个给定的构造函数或静态工厂方法(即使是包级私有的)在从数据库实例化对象时使用。 构造函数参数通过名称映射到所检索行中的值。spring-doc.cadn.net.cn

  • @Value:此注解是 Spring 框架的一部分。 在映射框架中,它可以应用于构造函数参数。 这允许你使用 Spring 表达式语言(SpEL)语句,在从数据库检索到某个键的值之后、用于构造领域对象之前对其进行转换。 为了引用给定行中的某一列,必须使用类似如下的表达式:@Value("#root.myProperty"),其中 root 指向给定 Row 的根。spring-doc.cadn.net.cn

  • @Column:应用于字段级别,用于描述该字段在数据库行中所对应的列名,允许列名与类中的字段名不同。 使用 @Column 注解指定的名称在 SQL 语句中始终会被加上引号。 对于大多数数据库而言,这意味着这些名称是区分大小写的。 这也意味着你可以在这些名称中使用特殊字符。 然而,我们不建议这样做,因为这可能会与其他工具产生兼容性问题。spring-doc.cadn.net.cn

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

参见乐观锁以获取更多参考信息。spring-doc.cadn.net.cn

映射元数据基础设施定义在独立的、与技术无关的 spring-data-commons 项目中。 JDBC 支持中使用了特定的子类来支持基于注解的元数据。 也可以根据需求采用其他策略。spring-doc.cadn.net.cn

引用的实体

对引用实体的处理是有限制的。 这是基于上文所述的聚合根(aggregate roots)的概念。 如果你引用了另一个实体,那么根据定义,该实体就是你聚合的一部分。 因此,如果你移除了这个引用,之前被引用的实体就会被删除。 这也意味着引用关系只能是一对一(1-1)或一对多(1-n),而不能是多对一(n-1)或多对多(n-m)。spring-doc.cadn.net.cn

如果你拥有 n-1 或 n-m 的引用,根据定义,你正在处理两个独立的聚合。 这些聚合之间的引用可以编码为简单的 id 值,Spring Data JDBC 能够正确地映射它们。 更好的方式是将这些引用编码为 AggregateReference 的实例。 AggregateReference 是一个围绕 id 值的包装器,用于将该值标记为对另一个聚合的引用。 此外,该聚合的类型会通过一个类型参数进行编码。spring-doc.cadn.net.cn

向后引用

聚合中的所有引用都会在数据库中反向生成外键关系。 默认情况下,外键列的名称为引用实体的表名。spring-doc.cadn.net.cn

如果所引用的 id 是一个 @Embedded id,则反向引用由多个列组成,每个列的名称由 <表名> + _ + <列名> 拼接而成。 例如,对一个 Person 实体的反向引用,若其复合主键包含 firstNamelastName 属性,则将由 PERSON_FIRST_NAMEPERSON_LAST_NAME 这两个列组成。spring-doc.cadn.net.cn

或者,您可以选择让外键名称基于引用实体的实体名称,而忽略 @Table 注解。 您可以通过在 setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING) 上调用 RelationalMappingContext 来启用此行为。spring-doc.cadn.net.cn

对于 ListMap 引用,需要一个额外的列来保存列表索引或映射键(map key)。 该列基于外键列,并附加 _KEY 后缀。spring-doc.cadn.net.cn

如果你希望以一种完全不同的方式命名这些反向引用,可以按照自己的需求实现 NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner) 方法。spring-doc.cadn.net.cn

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

// ...

Person p1, p2 = // some initialization

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

你不应在实体中包含用于保存反向引用的实际值,也不应包含用于映射(Map)或列表(List)的键列值的属性。 如果你希望在领域模型中使用这些值,我们建议在 AfterConvertCallback 中进行处理,并将这些值存储在瞬态(transient)字段中。spring-doc.cadn.net.cn

命名策略

按照约定,Spring Data 应用 NamingStrategy 来确定表、列和模式名称,默认采用 蛇形命名法(snake case)。 名为 firstName 的对象属性将变为 first_name。 您可以在应用程序上下文中提供 NamingStrategy 来调整这一行为。spring-doc.cadn.net.cn

覆盖表名

当表命名策略与您的数据库表名不匹配时,您可以使用 Table 注解覆盖表名。 该注解的元素 value 提供自定义表名。 以下示例将 MyEntity 类映射到数据库中的 CUSTOM_TABLE_NAME 表:spring-doc.cadn.net.cn

@Table("CUSTOM_TABLE_NAME")
class MyEntity {
    @Id
    Integer id;

    String name;
}

您可以使用Spring Data 的 SpEL 支持来动态创建表名。 一旦生成,表名将被缓存,因此它仅在每个映射上下文中是动态的。spring-doc.cadn.net.cn

覆盖列名

当列命名策略与您的数据库表名不匹配时,您可以使用 Column 注解来覆盖表名。 该注解的 value 元素提供自定义列名。 以下示例将 MyEntity 类的 name 属性映射到数据库中的 CUSTOM_COLUMN_NAME 列:spring-doc.cadn.net.cn

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 列对应其 id:spring-doc.cadn.net.cn

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 元素进行自定义:spring-doc.cadn.net.cn

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 支持来动态创建列名。 一旦生成,这些名称将被缓存,因此仅在每个映射上下文中具有动态性。spring-doc.cadn.net.cn

嵌入实体

嵌入式实体用于在您的 Java 数据模型中使用值对象,即使数据库中只有一张表。 在下面的示例中,您可以看到 MyEntity 使用了 @Embedded 注解进行映射。 这样做的结果是,数据库中会期望存在一张名为 my_entity 的表,该表包含两个列:idname(来自 EmbeddedEntity 类)。spring-doc.cadn.net.cn

然而,如果name列实际上是null在结果集中,整个属性embeddedEntity将根据onEmpty of @Embedded,即null当所有嵌套属性都是 s 对象时null.
与此行为相反USE_EMPTY尝试使用默认构造函数或接受结果集中可空参数值的构造函数来创建新实例。spring-doc.cadn.net.cn

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

    @Id
    Integer id;

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

class EmbeddedEntity {
    String name;
}
1 如果 NullembeddedEntity,则将 name 设为 null。 使用 USE_EMPTY 来实例化 embeddedEntity,其 null 属性可能为 name

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

使用快捷注解 @Embedded.Nullable@Embedded.Empty 分别代替 @Embedded(onEmpty = USE_NULL)@Embedded(onEmpty = USE_EMPTY),以减少冗长代码,并同时相应地设置 JSR-305 的 @javax.annotation.Nonnull 注解。spring-doc.cadn.net.cn

class MyEntity {

    @Id
    Integer id;

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

包含 CollectionMap 的嵌入实体将始终被视为非空,因为它们至少会包含一个空的集合或映射。 因此,即使使用 @Embedded(onEmpty = USE_NULL),此类实体也永远不会为 nullspring-doc.cadn.net.cn

嵌入的 ID

标识符属性可以使用 @Embedded 注解,从而允许使用复合主键(composite ids)。 整个嵌入的实体被视为 ID,因此在判断一个聚合是新聚合(需要执行插入操作)还是已有聚合(需要执行更新操作)时,依据的是该实体本身,而不是其内部的各个元素。 大多数使用场景都需要自定义一个 BeforeConvertCallback,以便为新的聚合设置 ID。spring-doc.cadn.net.cn

具有复合主键的简单实体
@Table("PERSON_WITH_COMPOSITE_ID")
record Person( (1)
    @Id @Embedded.Nullable Name pk, (2)
    String nickName,
    Integer age
) {
}

record Name(String first, String last) {
}
具有复合主键的简单实体的匹配表
CREATE TABLE PERSON_WITH_COMPOSITE_ID (
    FIRST VARCHAR(100),
    LAST VARCHAR(100),
    NICK_NAME VARCHAR(100),
    AGE INT,
    PRIMARY KEY (FIRST, LAST) (3)
);
1 实体可以表示为记录,而无需任何特殊考虑。
2 pk 被标记为 id 并嵌入
3 嵌入的Name实体中的两个列构成了数据库中的主键。

表的创建细节取决于所使用的数据库。spring-doc.cadn.net.cn

只读属性

使用 @ReadOnlyProperty 注解的属性不会被 Spring Data 写入数据库,但在加载实体时会被读取。spring-doc.cadn.net.cn

Spring Data 在写入实体后不会自动重新加载该实体。 因此,如果你希望看到数据库为此类列生成的数据,必须显式地重新加载它。spring-doc.cadn.net.cn

如果被注解的属性是一个实体或实体集合,则它在单独的表中由一行或多行表示。 Spring Data 不会对这些行执行任何插入、删除或更新操作。spring-doc.cadn.net.cn

仅插入属性

使用 @InsertOnlyProperty 注解的属性仅在插入操作期间由 Spring Data 写入数据库。 在更新操作中,这些属性将被忽略。spring-doc.cadn.net.cn

@InsertOnlyProperty 仅支持聚合根。spring-doc.cadn.net.cn

自定义对象构建

映射子系统允许通过使用 @PersistenceConstructor 注解标注构造函数来定制对象的构造方式。构造函数参数所使用的值将按以下方式解析:spring-doc.cadn.net.cn

  • 如果一个参数使用 @Value 注解进行标注,则会计算给定的表达式,并将结果用作该参数的值。spring-doc.cadn.net.cn

  • 如果 Java 类型具有一个属性,其名称与输入行中的给定字段相匹配,那么将使用该属性的信息来选择合适的构造函数参数,以传递输入字段的值。 这仅在 Java .class 文件中包含参数名称信息时才有效,您可以通过使用调试信息编译源代码,或在 Java 8 中使用 -parametersjavac 命令行选项来实现这一点。spring-doc.cadn.net.cn

  • 否则,将抛出一个 MappingException,以表明给定的构造函数参数无法被绑定。spring-doc.cadn.net.cn

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-doc.cadn.net.cn

使用已注册的 Spring 转换器写入属性

以下示例展示了一个 Converter 的实现,该实现将 Boolean 对象转换为 String 值:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

使用 Spring 转换器进行读取

以下示例展示了一个 Converter 的实现,该实现将 String 转换为 Boolean 值:spring-doc.cadn.net.cn

@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 将数据库中所有应赋值给 String 属性的 Boolean 值进行转换。spring-doc.cadn.net.cn

注册 Spring 转换器与JdbcConverter

class MyJdbcConfiguration extends AbstractJdbcConfiguration {

    // …

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

}
在 Spring Data JDBC 的早期版本中,建议直接重写 AbstractJdbcConfiguration.jdbcCustomConversions() 方法。 现在已不再需要,甚至不建议这样做,因为该方法会整合适用于所有数据库的转换器、所使用的 JdbcDialect 注册的转换器以及用户注册的转换器。 如果您从旧版本的 Spring Data JDBC 迁移而来,并且重写了 AbstractJdbcConfiguration.jdbcCustomConversions() 方法,那么您所使用的 JdbcDialect 中注册的转换器将不会被注册。

如果你想依赖Spring Boot来引导Spring Data JDBC,但又希望覆盖某些配置方面,你可能需要暴露该类型的bean。 例如,对于自定义转换,你可以选择注册一个类型为JdbcCustomConversions的bean,该bean会被Boot基础设施自动识别。 要了解更多相关内容,请务必阅读Spring Boot参考文档spring-doc.cadn.net.cn

JdbcValue

值转换使用 JdbcValue 来为传递给 JDBC 操作的值附加一个 java.sql.Types 类型。 如果需要指定特定于 JDBC 的类型而非使用类型推导,请注册一个自定义的写入转换器。 该转换器应将值转换为 JdbcValue,后者包含一个用于存储值的字段和一个用于存储实际 JDBCType 的字段。spring-doc.cadn.net.cn