映射

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

本节描述了 MappingJdbcConverter 的功能,包括如何使用约定将对象映射到行以及如何使用基于注解的映射元数据覆盖这些约定。

在继续阅读本章之前,请先阅读有关 对象映射基础 的基本知识。

基于约定的映射

当没有提供额外的映射元数据时,MappingJdbcConverter 有几个将对象映射到行的约定:

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

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

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

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

实体中支持的类型

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

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

  • 枚举映射到其名称。

  • 字符串

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

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

  • 数据库驱动程序接受的任何内容。

  • 对其他实体的引用。它们被视为一对一关系或嵌入类型。一对一关系实体可选地具有 id 属性。引用的实体的表预计会包含一个附加列,其名称基于引用实体,请参阅 反向引用。嵌入式实体不需要 id。如果存在,它将作为普通属性映射,没有任何特殊含义。

  • Set<some entity> 被视为一对多关系。引用的实体的表预计会包含一个附加列,其名称基于引用实体,请参阅 反向引用

  • Map<simple type, some entity> 被视为限定的一对多关系。引用的实体的表预计会有两列:一列基于引用实体的名称作为外键(请参阅 反向引用),另一列名称相同,并附加 _key 后缀作为映射键。

  • List<some entity> 映射为 Map<Integer, some entity>。预期相同的附加列,并且可以使用相同的方式自定义使用的名称。

对于 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 Framework 的一部分。在映射框架中,它可以应用于构造函数参数。这允许你使用 Spring 表达式语言语句转换从数据库检索到的键值,然后将其用于构造领域对象。为了引用给定行的列,必须使用类似这样的表达式:@Value("#root.myProperty"),其中 root 指的是给定 Row 的根。

  • @Column:应用于字段级别,用于描述列在行中表示的名称,允许列名与类字段名不同。使用 @Column 注解指定的名称在 SQL 语句中始终加引号。对于大多数数据库来说,这意味着这些名称区分大小写。这也意味着你可以在这些名称中使用特殊字符。但是,不推荐这样做,因为它可能会导致其他工具出现问题。

  • @Version:应用于字段级别,用于乐观锁,并在保存操作中检查修改。该值为 null(对于基本类型为 )被视为新实体的标记。最初存储的值为 (对于基本类型为 )。版本在每次更新时都会自动递增。

更多信息,请参见 乐观锁

映射元数据基础设施定义在单独的 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),以适应你的需求。

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

// ...

Person p1, p2 = // some initialization

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

你不应该在实体中包含属性来保存反向引用的实际值,也不保存映射或列表的键列的值。如果你希望这些值在你的领域模型中可用,我们建议在 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 注解可用于引用类型(一对一关系)或集合、列表和映射(一对多关系)。注解的 idColumn 元素为引用另一表中 id 列的外键列提供自定义名称。在下面的示例中,MySubEntity 类的相应表具有 NAME 列,而 MyEntityCUSTOM_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;
}

当使用 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;
}

你可以使用 Spring Data 的 SpEL 支持 来动态创建列名。名称生成后将被缓存,因此它仅对每个映射上下文都是动态的。

嵌入式实体

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

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

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

    @Id
    Integer id;

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

class EmbeddedEntity {
    String name;
}
1 如果 namenull,则 embeddedEntitynull。使用 USE_EMPTYname 属性的潜在 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;
}
1 @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
}

使用显式转换器覆盖映射

Spring Data允许注册自定义转换器来影响数据库中值映射的方式。目前,转换器仅应用于属性级别,即您只能将域中的单个值转换为数据库中的单个值,反之亦然。不支持复杂对象和多个列之间的转换。

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

以下示例显示了将Boolean对象转换为String值的Converter实现。

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转换器读取

以下示例显示了将String转换为Boolean值的Converter实现。

@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转换数据库中应分配给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,该bean将被Boot基础结构拾取。要了解更多信息,请务必阅读Spring Boot的参考文档

JdbcValue

值转换使用JdbcValue来丰富传播到JDBC操作的值,并使用java.sql.Types类型。如果您需要指定JDBC特定的类型而不是使用类型派生,请注册自定义写入转换器。此转换器应将值转换为JdbcValue,它具有值字段和实际的JDBCType字段。