映射
MappingJdbcConverter 提供了丰富的映射支持。MappingJdbcConverter 拥有丰富的元数据模型,可将领域对象映射到数据行。映射元数据模型通过在您的领域对象上使用注解来填充。不过,基础设施并不局限于将注解作为元数据信息的唯一来源。MappingJdbcConverter 还允许您遵循一组约定,在不提供任何额外元数据的情况下将对象映射到行。
本节介绍 MappingJdbcConverter 的功能,包括如何使用约定将对象映射到行,以及如何使用基于注解的映射元数据来覆盖这些约定。
在继续阅读本章之前,请先阅读有关对象映射基础的内容。
基于约定的映射
当未提供额外的映射元数据时,MappingJdbcConverter 有若干将对象映射到行的约定。这些约定是:
-
Java 短类名按以下方式映射到表名。
com.bigbank.SavingsAccount类映射到SAVINGS_ACCOUNT表名。字段到列名的映射也采用相同的命名方式。例如,firstName字段映射到FIRST_NAME列。您可以通过提供自定义NamingStrategy来控制此映射。更多细节请参阅命名策略。默认情况下,从属性名或类名派生的表名和列名在 SQL 语句中会带引号使用。您可以通过设置JdbcMappingContext.setForceQuote(false)来控制此行为,或使用JdbcMappingContext.forPlainIdentifiers()创建一个使用普通(不带引号)标识符的上下文。 -
转换器使用注册到
CustomConversions中的任意 Spring 转换器,来覆盖对象属性到行列和列值的默认映射。 -
对象的字段用于在行的列之间进行转换。公共
JavaBean属性不被使用。 -
如果您有一个非零参数构造函数,且其构造函数参数名与行的顶级列名匹配,则使用该构造函数。否则使用零参数构造函数。如果存在多个非零参数构造函数,则抛出异常。更多细节请参阅对象创建。
实体中支持的类型
目前支持以下类型的属性:
-
所有基本类型及其包装类型(
int、float、Integer、Float等) -
枚举映射为其名称。
-
String -
java.util.Date、java.time.LocalDate、java.time.LocalDateTime和java.time.LocalTime -
如果您的数据库支持,上述类型的数组和集合可以映射为数组类型的列。
-
您的数据库驱动程序接受的任何类型。
-
对其他实体的引用。它们被视为一对一关系或内嵌类型。一对一关系实体可以选择性地拥有
id属性。被引用实体的表应包含一个额外列,其名称基于引用实体,参见反向引用。内嵌实体不需要id。如果存在,则将其作为普通属性映射,没有特殊含义。 -
Set<某实体>被视为一对多关系。被引用实体的表应包含一个额外列,其名称基于引用实体,参见反向引用。 -
Map<简单类型, 某实体>被视为带限定的一对多关系。被引用实体的表应包含两个额外列:一个基于引用实体命名,用于外键(参见反向引用);另一个同名并附加_key后缀,用于映射键。 -
List<某实体>映射为Map<Integer, 某实体>。同样需要这些额外列,且可用相同方式自定义所使用的名称。对于
List、Set和Map,反向引用的命名可分别通过实现NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner)和NamingStrategy.getKeyColumn(RelationalPersistentProperty property)来控制。或者,您可以用@MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name")注解标注该属性。为Set指定键列没有效果。 -
您已为其注册了合适的自定义转换器的类型。
映射注解概述
RelationalConverter 可以使用元数据来驱动对象到行的映射。以下注解可用:
-
@Embedded:带有此注解的属性将映射到父实体的表,而不是单独的表。可指定生成的列是否应有共同前缀。如果由此类实体产生的所有列都为null,则被注解的实体将为null或_empty_(即其所有属性都为null),取决于@Embedded.onEmpty()的值。可与@Id组合形成组合 id。 -
@Id:在字段级别应用,用于标记主键。可与@Embedded组合形成组合 id。 -
@InsertOnlyProperty:将属性标记为仅在插入时写入。聚合根上的此类属性仅写入一次,永不更新。请注意,在嵌套实体上,所有保存操作都会导致插入,因此此注解对嵌套实体的属性无效。 -
@MappedCollection:用于配置集合或单个嵌套实体如何映射。idColumn指定用于引用父实体主键的列。keyColumn指定用于存储List的索引或Map的键的列。 -
@Sequence:指定一个数据库序列,用于为被注解的属性生成值。 -
@Table:在类级别应用,表示该类是映射到数据库的候选。您可以指定数据库存储的表名。 -
@Transient:默认情况下,所有字段都映射到行。此注解将其应用的字段排除在数据库存储之外。瞬态属性不能在持久化构造函数中使用,因为转换器无法为该构造函数参数生成值。 -
@PersistenceCreator:标记给定的构造函数或静态工厂方法——即使是包级私有的——用于在从数据库实例化对象时使用。构造函数参数按名称映射到检索到的行中的值。 -
@Value:此注解是 Spring Framework 的一部分。在映射框架内,它可应用于构造函数参数。这让您可以使用 Spring 表达式语言语句,在数据库中检索到的键值用于构造领域对象之前对其进行转换。为了引用给定行的某一列,必须使用如下表达式:@Value("#root.myProperty"),其中 root 指给定Row的根。 -
@Column:在字段级别应用,用于描述该列在行中所表示的名称,让名称可以不同于类的字段名。当forceQuote启用时(默认),用@Column注解指定的名称会被加引号。对于大多数数据库,加引号意味着这些名称区分大小写。这也意味着您可以在这些名称中使用特殊字符。但不建议这样做,因为它可能与其他工具产生问题。 -
@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 值的包装,将该值标记为对另一个聚合的引用。此外,该聚合的类型编码在一个类型参数中。
反向引用
聚合中的所有引用都会在数据库中产生一个方向相反的外键关系。默认情况下,外键列的名称是引用实体的表名。
如果被引用的 id 是 @Embedded id,则反向引用由多个列组成,每列名称由 <表名> + _ + <列名> 拼接而成。例如,对 Person 实体的反向引用(其组合 id 具有 firstName 和 lastName 属性)将由 PERSON_FIRST_NAME 和 PERSON_LAST_NAME 两列组成。
或者,您可以选择忽略 @Table 注解,改用引用实体的实体名来命名这些列。您通过在 DefaultNamingStrategy 上调用 setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING) 来启用此行为。
对于 List 和 Map 引用,需要一个额外列来保存列表索引或映射键。它基于外键列并附加 _KEY 后缀。
如果您希望以完全不同的方式命名这些反向引用,可以按适合您需求的方式实现 NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner)。
class Person {
@Id long id;
AggregateReference<Person, Long> bestFriend;
}
// ...
Person p1, p2 = // some initialization
p1.bestFriend = AggregateReference.to(p2.id);
您不应在实体中包含属性来保存反向引用的实际值,也不应保存映射或列表键列的值。如果您希望这些值在领域模型中可用,我们建议在 AfterConvertCallback 中完成,并将这些值存储在瞬态属性中。
命名策略
默认情况下,Spring Data 应用 NamingStrategy 来确定表、列和模式名称,默认为 snake_case。名为 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 注解可用于 Set、List 和 Map(一对多关系)。该注解的 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;
}
使用 List 和 Map 时,您必须有一个额外列,用于保存数据在 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 表,包含 id 和 name 两列(来自 EmbeddedEntity 类)。
然而,如果 name 列在结果集中实际为 null,则整个 embeddedEntity 属性将根据 @Embedded 的 onEmpty 被设为 null,当所有嵌套属性都为 null 时它会将对象置为 null。
与此相反,USE_EMPTY 会尝试使用默认构造函数或接受结果集中可空参数值的构造函数来创建新实例。
示例 1. 内嵌对象的示例代码
class MyEntity {
@Id
Integer id;
@Embedded(onEmpty = USE_NULL) 1
EmbeddedEntity embeddedEntity;
}
class EmbeddedEntity {
String name;
}
如果
name为null,则将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;
}
@Embedded(onEmpty = USE_NULL)的快捷方式。
包含 Collection 或 Map 的内嵌实体始终被视为非空,因为它们至少会包含空集合或空映射。因此,即使用 @Embedded(onEmpty = USE\_NULL),此类实体也绝不会为 null。
内嵌 Id
标识符属性可以用 @Embedded 注解,从而使用组合 id。整个内嵌实体被视为 id,因此判断一个聚合是需要插入的新聚合、还是需要更新的已有聚合,是基于该实体而非其元素进行的。大多数用例都需要自定义 BeforeConvertCallback 来为新聚合设置 id。
@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
);
实体可以表示为 record,无需任何特殊处理。
pk被标记为 id 并内嵌来自内嵌
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
}
用显式转换器覆盖映射
Spring Data 允许注册自定义转换器来影响值在数据库中的映射方式。目前,转换器仅在属性级别应用,即您只能将领域中的单个值转换为数据库中的单个值,反之亦然。不支持复杂对象与多列之间的转换。
使用注册的 Spring 转换器写入属性
以下示例展示了一个 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";
}
}
这里有几点需要注意:Boolean 和 String 都是简单类型,因此 Spring Data 需要一个提示来指明此转换器应在哪个方向应用(读取或写入)。通过用 @WritingConverter 注解标注此转换器,您指示 Spring Data 将每个 Boolean 属性在数据库中写为 String。
使用 Spring 转换器读取
以下示例展示了一个 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;
}
}
这里有几点需要注意:String 和 Boolean 都是简单类型,因此 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()。现在不再需要、甚至不推荐这样做,因为该方法会组装面向所有数据库的转换、由所用 JdbcDialect 注册的转换以及由用户注册的转换。如果您从旧版本的 Spring Data JDBC 迁移,并且覆盖了 AbstractJdbcConfiguration.jdbcCustomConversions(),那么来自您的 JdbcDialect 的转换将不会被注册。
如果您想依赖 Spring Boot 来初始化 Spring Data JDBC,但仍想覆盖配置的某些方面,您可能希望暴露该类型的 bean。对于自定义转换,例如您可以选择注册一个 JdbcCustomConversions 类型的 bean,它会被 Boot 基础设施拾取。要了解更多,请务必阅读 Spring Boot 参考文档。
JdbcValue
值转换使用 JdbcValue 来为传播到 JDBC 操作的值附加一个 java.sql.Types 类型。如果您需要指定 JDBC 特定类型而非使用类型推导,请注册一个自定义写入转换器。该转换器应将值转换为 JdbcValue,它有一个字段存放值,另一个字段存放实际的 JDBCType。