跳到主要内容

映射

DeepSeek V3 中英对照 Mapping

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

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

在继续本章之前,请先阅读对象映射基础的相关内容。

基于约定的映射

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

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

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

  • 对象的字段用于与行中的列进行转换。不会使用公共的 JavaBean 属性。

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

实体中支持的类型

当前支持以下类型的属性:

  • 所有原始类型及其包装类型(如 intfloatIntegerFloat 等)

  • 枚举类型会被映射为其名称。

  • String

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

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

  • 数据库驱动程序接受的任何类型。

  • 对其他实体的引用。它们被视为一对一关系或嵌入类型。一对一关系的实体可以没有 id 属性。被引用实体的表应包含一个基于引用实体命名的附加列,详见 反向引用。嵌入实体不需要 id。如果存在 id,它会被映射为一个普通属性,没有任何特殊含义。

  • Set<某个实体> 被视为一对多关系。被引用实体的表应包含一个基于引用实体命名的附加列,详见 反向引用

  • Map<简单类型, 某个实体> 被视为限定的一对多关系。被引用实体的表应包含两个附加列:一个基于引用实体命名的外键列(详见 反向引用),以及一个与列名相同并附加 _key 后缀的映射键列。

  • List<某个实体> 被映射为 Map<Integer, 某个实体>。同样需要附加列,且命名方式可以自定义。

    对于 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) 方法。

class Person {
@Id long id;
AggregateReference<Person, Long> bestFriend;
}

// ...

Person p1, p2 = // some initialization

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

你不应该在实体中包含属性来保存反向引用的实际值,或者保存映射或列表的键列的值。如果你希望这些值在你的领域模型中可用,我们建议在 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;
}
java

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

重写列名

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

class MyEntity {
@Id
Integer id;

@Column("CUSTOM_COLUMN_NAME")
String name;
}
java

MappedCollection 注解可以用于引用类型(一对一关系)或 SetListMap(一对多关系)。该注解的 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;
}
java

在使用 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

你可以使用 Spring Data 的 SpEL 支持 来动态生成列名。一旦生成,这些名称将被缓存,因此仅在每个映射上下文中是动态的。

嵌入式实体

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

然而,如果结果集中的 name 列实际为 null,根据 @EmbeddedonEmpty 属性,整个 embeddedEntity 属性将被设置为 null,这是因为当所有嵌套属性都为 null 时,@Embedded 会将对象置为 null
与此行为相反,USE_EMPTY 会尝试使用默认构造函数或接受结果集中可空参数值的构造函数来创建一个新实例。

示例 1. 嵌入对象的示例代码

class MyEntity {

@Id
Integer id;

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

class EmbeddedEntity {
String name;
}
java
  • 如果 namenull,则 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;
}
java
  • @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
}
java

使用显式转换器覆盖映射

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";
}
}
java

这里有几件事需要注意:BooleanString 都是简单类型,因此 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;
}
}
java

这里有几件事情需要注意: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());
}

}
java
备注

在早期版本的 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