映射
MappingJdbcConverter 提供了丰富的映射支持。MappingJdbcConverter 拥有丰富的元数据模型,允许将领域对象映射到数据行。映射元数据模型通过在领域对象上使用注解来填充。然而,基础设施不限于将注解作为元数据信息的唯一来源。MappingJdbcConverter 还允许您通过遵循一组约定,在不提供任何额外元数据的情况下将对象映射到行。
本节介绍了 MappingJdbcConverter 的功能,包括如何使用约定将对象映射到行,以及如何通过基于注解的映射元数据覆盖这些约定。
在继续本章之前,请阅读有关对象映射基础的基础知识。
基于约定的映射
当未提供额外映射元数据时,MappingJdbcConverter 有一些将对象映射到行的约定。这些约定是
-
Java 类短名称以下列方式映射到表名。
com.bigbank.SavingsAccount类映射到SAVINGS_ACCOUNT表名。相同的名称映射也适用于字段到列名的映射。例如,firstName字段映射到FIRST_NAME列。您可以通过提供自定义NamingStrategy来控制此映射。有关详细信息,请参阅映射配置。默认情况下,从属性或类名派生的表名和列名在 SQL 语句中不带引号使用。您可以通过设置RelationalMappingContext.setForceQuote(true)来控制此行为。 -
转换器使用注册到
CustomConversions的任何 Spring 转换器来覆盖对象属性到行、列和值的默认映射。 -
对象的字段用于在行中进行列的转换。不使用公共
JavaBean属性。 -
如果您有一个单一的非零参数构造函数,其构造函数参数名称与行的顶级列名匹配,则使用该构造函数。否则,使用零参数构造函数。如果有多个非零参数构造函数,则抛出异常。有关详细信息,请参阅对象创建。
实体中支持的类型
目前支持以下类型的属性
-
所有基本类型及其包装类型(
int、float、Integer、Float等) -
枚举映射到它们的名称。
-
字符串 -
java.util.Date、java.time.LocalDate、java.time.LocalDateTime和java.time.LocalTime -
如果您的数据库支持数组类型,则上述类型的数组和集合可以映射到数组类型的列。
-
您的数据库驱动程序接受的任何类型。
-
对其他实体的引用。它们被视为一对一关系或嵌入类型。对于一对一关系实体,
id属性是可选的。被引用实体的表预计有一个额外的列,其名称基于引用实体,请参阅反向引用。嵌入实体不需要id。如果存在,它将作为普通属性映射,没有任何特殊含义。 -
Set<some entity>被视为一对多关系。被引用实体的表预计有一个额外的列,其名称基于引用实体,请参阅反向引用。 -
Map<simple type, some entity>被视为限定的一对多关系。被引用实体的表预计有两个额外的列:一个基于引用实体用于外键(参见反向引用),另一个具有相同的名称和额外的_key后缀用于 map 键。 -
List<some entity>映射为Map<Integer, some entity>。需要相同的附加列,并且使用的名称可以以相同的方式进行自定义。对于
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,则根据@Embedded.onEmpty()的值,被注解的实体将为null或“空”,即其所有属性都将为null。可以与@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:应用于字段级别,描述列在行中的名称,允许名称与类的字段名称不同。用@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 值的包装器,它将该值标记为对不同聚合的引用。此外,该聚合的类型在类型参数中编码。
反向引用
聚合中的所有引用都会导致数据库中相反方向的外键关系。默认情况下,外键列的名称是引用实体的表名。
如果被引用的 ID 是 @Embedded ID,则反向引用由多个列组成,每个列的名称由 <table-name> + _ + <column-name> 拼接而成。例如,对一个 Person 实体(具有包含 firstName 和 lastName 属性的复合 ID)的反向引用将由两个列 PERSON_FIRST_NAME 和 PERSON_LAST_NAME 组成。
另外,您可以选择根据引用实体的实体名称来命名它们,忽略 @Table 注解。您可以通过在 RelationalMappingContext 上调用 setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING) 来激活此行为。
对于 List 和 Map 引用,需要一个额外的列来保存列表索引或 map 键。它基于外键列,并带有额外的 _KEY 后缀。
如果您需要完全不同的命名这些反向引用的方式,您可以按照自己的需求实现 NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner)。
AggregateReferenceclass Person {
@Id long id;
AggregateReference<Person, Long> bestFriend;
}
// ...
Person p1, p2 = // some initialization
p1.bestFriend = AggregateReference.to(p2.id);
您不应该在实体中包含属性来保存反向引用的实际值,也不应保存 map 或 list 的键列。如果您希望这些值在您的领域模型中可用,我们建议在 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;
}
您可以使用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,则根据 @Embedded 的 onEmpty 属性,整个 embeddedEntity 属性将被设置为 null,该属性在所有嵌套属性都为 null 时将对象设置为 null。
与此行为相反,USE_EMPTY 尝试使用默认构造函数或接受结果集中可空参数值的构造函数创建新实例。
class MyEntity {
@Id
Integer id;
@Embedded(onEmpty = USE_NULL) (1)
EmbeddedEntity embeddedEntity;
}
class EmbeddedEntity {
String name;
}
| 1 | 如果 name 为 null,则 embeddedEntity 为 null。使用 USE_EMPTY 实例化 embeddedEntity,其中 name 属性可能为 null。 |
如果您在实体中需要多次使用值对象,这可以通过 @Embedded 注解的可选 prefix 元素实现。此元素表示一个前缀,并预先添加到嵌入对象中的每个列名。
|
使用快捷方式
|
包含 Collection 或 Map 的嵌入式实体将始终被视为非空,因为它们至少会包含空集合或 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)
);
| 1 | 实体可以表示为记录,无需任何特殊考虑。 |
| 2 | pk 被标记为 id 并嵌入 |
| 3 | 嵌入式 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 值进行转换。
将 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。对于自定义转换,您可以选择注册一个 |