映射
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
等) -
枚举类型会被映射为其名称。
-
String
-
java.util.Date
、java.time.LocalDate
、java.time.LocalDateTime
和java.time.LocalTime
-
如果数据库支持,上述类型的数组和集合可以映射到数组类型的列。
-
数据库驱动程序接受的任何类型。
-
对其他实体的引用。它们被视为一对一关系或嵌入类型。一对一关系的实体可以没有
id
属性。被引用实体的表应包含一个基于引用实体命名的附加列,详见 反向引用。嵌入实体不需要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
可以使用元数据来驱动对象到行的映射。以下是可用的注解:
-
@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)
来激活此行为。
对于 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
,根据 @Embedded
的 onEmpty
属性,整个 embeddedEntity
属性将被设置为 null
,这是因为当所有嵌套属性都为 null
时,@Embedded
会将对象置为 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
来实例化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;
}
@Embedded(onEmpty = USE_NULL)
的快捷方式。
包含 Collection
或 Map
的嵌入式实体将始终被视为非空,因为它们至少会包含空集合或空映射。因此,即使在使用 @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
的实现,它将 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()
方法。然而,现在不再需要甚至不推荐这样做,因为该方法会组装适用于所有数据库的转换、由所使用的 Dialect
注册的转换以及用户注册的转换。如果你正在从较旧版本的 Spring Data JDBC 迁移,并且已经覆盖了 AbstractJdbcConfiguration.jdbcCustomConversions()
,那么来自你的 Dialect
的转换将不会被注册。
如果你想依赖 Spring Boot 来引导 Spring Data JDBC,但仍希望覆盖配置的某些方面,你可能需要暴露该类型的 bean。对于自定义转换,例如,你可以选择注册一个 JdbcCustomConversions
类型的 bean,它将被 Boot 基础设施自动拾取。要了解更多信息,请确保阅读 Spring Boot 的 参考文档。
JdbcValue
值转换使用 JdbcValue
来丰富传递给 JDBC 操作的值,并附带一个 java.sql.Types
类型。如果需要指定 JDBC 特定的类型而不是使用类型推导,请注册一个自定义的写入转换器。该转换器应将值转换为 JdbcValue
,其中包含一个字段用于存储值,另一个字段用于存储实际的 JDBCType
。