Mapping

BasicJdbcConverter 是一款强大的工具,它可以通过注释或约定从域对象映射到数据库行。它支持各种类型,包括基本类型、枚举、日期、列表和映射,以及对其他实体的引用。通过提供自定义转换器,可以覆盖默认映射并支持特定的数据类型转换。提及对聚合根的引用以及管理反向引用的最佳实践。

丰富的映射支持是由 BasicJdbcConverter 提供的。BasicJdbcConverter 有一个丰富的元数据模型,允许将域对象映射到数据行。映射元数据模型通过使用注释填充到你的域对象上。然而,基础架构并不局限于使用注释作为元数据信息的唯一来源。通过遵循一组约定,BasicJdbcConverter 还允许你在不提供任何其他元数据的情况下将对象映射到行。 本部分描述了 BasicJdbcConverter 的功能,包括如何使用约定将对象映射到行以及如何用基于注释的映射元数据覆盖这些约定。 在继续本章之前,阅读有关 object-mapping.adoc的基础知识。

Convention-based Mapping

当没有提供其他映射元数据时,BasicJdbcConverter 有几个约定可将对象映射到行。这些约定是:

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

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

  • 对象的字段用于转换到和从行中的列。公共 JavaBean 属性未使用。

  • 如果你只有一个非零参数构造函数,且其构造函数参数名称匹配行的顶级列名称,则使用该构造函数。否则,将使用零参数构造函数。如果有多于一个非零参数构造函数,则会引发异常。请参阅 Object Creation 了解更多详细信息。

Supported Types in Your Entity

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

  • 所有基本类型及其装箱类型(int,float,Integer,`Float`等等)

  • 枚举映射到它们的名称。

  • String

  • java.util.Date,java.time.LocalDate,java.time.LocalDateTime`和 `java.time.LocalTime

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

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

  • 对其他实体的引用。它们被视为一对一的关系或嵌套类型。一对一关系实体不必有 id 属性。引用实体的表预期有一个附加列,其名称基于引用实体,请参见 Back References。嵌套实体不需要 id。如果存在一个嵌套实体,则它会被映射为一个没有任何特殊含义的常规属性。

  • Set<some entity> 被视为一对多关系。引用实体的表预期有一个附加列,其名称基于引用实体,请参见 Back References

  • Map<simple type, some entity> 被视为一个限定的一对多关系。引用实体的表预期有两个附加列:一个以引用实体来为外键命名(请参见 Back References),另一个具有相同名称和一个附加 _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 指定一个键列没有任何作用。

Mapping Annotation Overview

`RelationalConverter`可以使用元数据来驱动对象到行的映射。下列注解是可用的:

  • @Id:应用于字段级别以标记主键。

  • @Table:应用于类级别以指示 此类 是映射到数据库的候选者。您可以指定存储数据库的表格的名称。

  • @Transient:默认情况下,所有字段都映射到行。这个批注将应用其排除字段从存储在数据库中。瞬态属性不能在持久化构造函数中使用,因为转换器无法使构造函数实参的值具体化。

  • @PersistenceCreator:标记一个给定的构造函数,甚至是包保护的,或者静态工厂方法 — 使用的时候从数据库实例化对象。构造函数实参通过名称映射到检索行中的值。

  • @Value:这个批注是 Spring 框架的一部分。在映射框架中它可以应用到构造函数实参。这使您能够使用 Spring 表达式语言语句来转换在数据库中检索的密钥值,然后才能用于构造域对象。为了引用给定行的列,必须使用以下表达式:@Value("#root.myProperty"),其中 root 指向给定 Row 的根。

  • @Column:应用于字段层级,用于描述列的名称,使其在行中显示为指定名称,允许该名称不同于类的字段名称。使用 @Column 注释指定的名称在 SQL 语句中使用时,始终会被引用。对于大多数数据库,这意味着这些名称区分大小写。这意味着你可以在这些名称中使用特殊字符。然而,不建议这样做,因为它可能会给其他工具带来麻烦。

  • @Version:应用于字段层级,用于乐观锁且在保存操作中进行修改检查。值是 null (对于原始类型为 zero),被视为新实体的标记。初始存储的值是 zero (对于原始类型为 one)。版本在每次更新时都会自动增加。

有关更多参考信息,请参见 Optimistic Locking

映射元数据基础设施在独立且与技术无关的 spring-data-commons 项目中定义。在 JDBC 支持中使用特定子类来支持基于注释的元数据。也可以实施其他策略(如果有需求)。

Referenced Entities

对引用的实体的处理是有限的。这是基于上述聚合根的思想。如果您引用另一个实体,则该实体根据定义是聚合的一部分。因此,如果您删除引用,则先前引用的实体将被删除。这也意味着引用是一对一或一对多,但不是多对一或多对多。

如果您有 n 对 1 或 n 对多引用,则根据定义,您正在处理两个单独的聚合。它们之间的引用可以编码为简单的 id 值,这些值可以通过 Spring Data JDBC 适当地映射。对这些引用进行编码的更好方法是将其设为 AggregateReference 的实例。AggregateReference 是对 id 值的包装,用于将该值标记为对其他聚合的引用。此外,将该聚合的类型编码在类型参数中。

Back References

聚合中的所有引用会导致数据库中相反方向的外键关系。默认情况下,外键列的名称是引用实体的表名。

或者,您可以选择使它们由引用实体的实体名称命名,而忽略 @Table 注释。通过在 RelationalMappingContext 上调用 setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING) 来激活此行为。

对于 ListMap 引用,需要一个附加列来保存列表索引或映射键。它基于外键列并附加 _KEY 后缀。

如果您希望完全不同的方式来命名这些反向引用,则可以根据需要实现 NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner)

Declaring and setting an AggregateReference
class Person {
	@Id long id;
	AggregateReference<Person, Long> bestFriend;
}

// ...

Person p1, p2 = // some initialization

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

不应在实体中包含属性来保存反向引用的实际值,也不应包含映射或列表的键列。如果您希望在域模型中提供这些值,我们建议在 AfterConvertCallback 中完成此操作,并将值存储在暂态值中。

  • 你已为其注册适当 [id="jdbc.custom-converters"][id="jdbc.custom-converters"] 的类型。

Naming Strategy

Naming Strategy

根据习惯,Spring Data 应用了一个 NamingStrategy,以确定表、列和模式名称,其默认为 snake case。名为 firstName`的对象属性会成为 `first_name。您可以通过在应用程序上下文中提供 {spring-data-jdbc-javadoc}org/springframework/data/relational/core/mapping/NamingStrategy.html[NamingStrategy] 来调整它。

Override table names

当表命名策略不匹配您的数据库表名称时,您可以使用 {spring-data-jdbc-javadoc}org/springframework/data/relational/core/mapping/Table.html[@Table] 注解覆盖表名称。此注解的 `value`元素提供了自定义表名称。以下示例将 `MyEntity`类映射到数据库中的 `CUSTOM_TABLE_NAME`表:

@Table("CUSTOM_TABLE_NAME")
class MyEntity {
    @Id
    Integer id;

    String name;
}

Override column names

当列命名策略不匹配您的数据库表名称时,您可以使用 {spring-data-jdbc-javadoc}org/springframework/data/relational/core/mapping/Column.html[@Column] 注解覆盖表名称。此注解的 `value`元素提供了自定义列名称。以下示例将 `MyEntity`类的 `name`属性映射到数据库中的 `CUSTOM_COLUMN_NAME`列:

class MyEntity {
    @Id
    Integer id;

    @Column("CUSTOM_COLUMN_NAME")
    String name;
}

Read Only Properties

使用 @ReadOnlyProperty 注解的属性不会被 Spring Data 写入到数据库中,但当加载实体时,这些属性将会被读取。

Spring Data 不会自动在写入实体后重新加载此实体。因此,如果你想要查看在数据库中为这些列生成的数据,那么就必须显式地重新加载此实体。

如果带注解的属性是实体或实体集合,则它将由各单独表中的一个或者若干单独行来表示。Spring Data 不会针对这些行执行任何插入、删除或者更新。

Insert Only Properties

使用 @InsertOnlyProperty 注解的属性仅在插入操作期间由 Spring Data 写入到数据库中。对于更新,将忽略这些属性。

@InsertOnlyProperty 仅受支持于聚合根。

Customized Object Construction

映射子系统允许通过使用 @PersistenceConstructor 注解注解构造函数来自定义对象构造。用于构造函数参数的值将按照以下方式解析:

  • 如果用 @Value 批注标注一个参数,则求值给定的表达式,结果用作参数值。

  • 如果 Java 类型具有与其输入行给定字段匹配的名称的属性,则其属性信息用于选择将输入字段值传递到的合适的构造函数参数。这仅在 Java 8 中的 Java .class 文件中存在参数名称信息时有效,您可以通过编译带有调试信息的源或使用 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
}

Overriding Mapping with Explicit Converters

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

Writing a Property by Using a Registered Spring Converter

以下示例显示了将 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

Reading by Using a Spring Converter

以下示例显示了将 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 值。

Registering Spring Converters with the JdbcConverter

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,基础设施将拾取它。要了解有关它的更多信息,请务必阅读 Spring Boot Reference Documentation

JdbcValue

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