對映

Spring Data Relational 透過 MappingJdbcConverter 提供豐富的對映支援。MappingJdbcConverter 具有豐富的元資料模型,允許將域物件對映到資料行。對映元資料模型透過使用域物件上的註解填充。然而,該基礎設施不限於僅使用註解作為元資料資訊的唯一來源。MappingJdbcConverter 還允許您在不提供任何額外元資料的情況下將物件對映到行,只需遵循一套約定。

本節介紹 MappingJdbcConverter 的特性,包括如何使用約定將物件對映到行以及如何使用基於註解的對映元資料覆蓋這些約定。

在繼續本章之前,請先閱讀關於物件對映基礎的基本知識。

基於約定的對映

當未提供額外對映元資料時,MappingJdbcConverter 有一些約定用於將物件對映到行。這些約定包括:

  • Java 類的短名稱以下列方式對映到表名。com.bigbank.SavingsAccount 類對映到 SAVINGS_ACCOUNT 表名。相同的名稱對映應用於欄位到列名的對映。例如,firstName 欄位對映到 FIRST_NAME 列。您可以透過提供自定義的 NamingStrategy 來控制此對映。更多詳細資訊請參閱對映配置。預設情況下,派生自屬性或類名的表名和列名在 SQL 語句中使用時沒有引號。您可以透過設定 RelationalMappingContext.setForceQuote(true) 來控制此行為。

  • 轉換器使用註冊到 CustomConversions 的任何 Spring Converter 來覆蓋物件屬性到行中的列和值的預設對映。

  • 物件的欄位用於在行中的列之間進行轉換。不使用公共的 JavaBean 屬性。

  • 如果您有一個非零引數的建構函式,且其建構函式引數名稱與行的頂級列名匹配,則使用該建構函式。否則,使用零引數建構函式。如果存在多個非零引數建構函式,則丟擲異常。更多詳情請參閱物件建立

實體中支援的型別

目前支援以下型別的屬性:

  • 所有基本型別及其包裝型別(如 int, float, Integer, Float 等)

  • 列舉型別對映到其名稱。

  • String

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

  • 上述型別的陣列和集合可以對映到陣列型別的列,如果您的資料庫支援的話。

  • 您的資料庫驅動程式接受的任何型別。

  • 對其他實體的引用。它們被視為一對一關係或嵌入型別。一對一關係實體可選包含 id 屬性。被引用實體的表預計會有一個額外的列,其名稱基於引用實體(見反向引用)。嵌入實體不需要 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:預設情況下,所有欄位都對映到行。此註解將應用的欄位從資料庫儲存中排除。Transient 屬性不能用於持久化建構函式中,因為轉換器無法為建構函式引數例項化值。

  • @PersistenceCreator:標記給定的建構函式或靜態工廠方法(即使是包私有的)在從資料庫例項化物件時使用。建構函式引數按名稱對映到檢索到的行中的值。

  • @Value:此註解是 Spring Framework 的一部分。在對映框架中,它可以應用於建構函式引數。這允許您使用 Spring Expression Language 語句轉換資料庫中檢索到的鍵值,然後將其用於構造域物件。為了引用給定行的列,必須使用如下表達式:@Value("#root.myProperty"),其中 root 指代給定 Row 的根。

  • @Column:應用於欄位級別,描述列在行中的名稱,允許名稱與類的欄位名稱不同。使用 @Column 註解指定的名稱在 SQL 語句中使用時總是帶引號。對於大多數資料庫,這意味著這些名稱區分大小寫。這也意味著您可以在這些名稱中使用特殊字元。但是,不建議這樣做,因為可能會導致與其他工具的問題。

  • @Version:應用於欄位級別,用於樂觀鎖,並在儲存操作時檢查修改。值為 null(對於基本型別為 zero)被視為新實體的標記。最初儲存的值為 zero(對於基本型別為 one)。每次更新時版本會自動遞增。

更多詳情請參閱樂觀鎖

對映元資料基礎設施在獨立的 spring-data-commons 專案中定義,該專案與技術無關。JDBC 支援中使用了特定的子類來支援基於註解的元資料。也可以採用其他策略(如果需要)。

引用實體

引用實體的處理是有限的。這是基於上述聚合根的理念。如果您引用另一個實體,該實體根據定義是您的聚合的一部分。因此,如果您移除引用,之前引用的實體將被刪除。這也意味著引用是 1 對 1 或 1 對多,而不是多對 1 或多對多。

如果您有多對 1 或多對多引用,根據定義,您正在處理兩個獨立的聚合。這些之間的引用可以編碼為簡單的 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 中完成,並將值儲存在 transient 值中。

命名策略

按照約定,Spring Data 應用 NamingStrategy 來確定表名、列名和 schema 名稱,預設為蛇形命名法。一個名為 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 註解可用於引用型別(一對一關係)或 Sets、Lists 和 Maps(一對多關係)。註解的 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;
}

使用 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 時,將物件設為 null),整個 embeddedEntity 屬性將被設為 null。
與此行為相反,USE_EMPTY 嘗試使用預設建構函式或接受結果集中可空引數值的建構函式來建立新例項。

示例 1. 嵌入物件的示例程式碼
class MyEntity {

    @Id
    Integer id;

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

class EmbeddedEntity {
    String name;
}
1 如果 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;
}
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 Converter 寫入屬性

以下示例展示了 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";
    }
}

這裡有幾點需要注意:BooleanString 都是簡單型別,因此 Spring Data 需要一個提示來確定此轉換器應應用於哪個方向(讀取或寫入)。透過使用 @WritingConverter 註解此轉換器,您指示 Spring Data 將每個 Boolean 屬性作為 String 寫入資料庫。

使用 Spring Converter 讀取

以下示例展示了 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;
    }
}

這裡有幾點需要注意:StringBoolean 都是簡單型別,因此 Spring Data 需要一個提示來確定此轉換器應應用於哪個方向(讀取或寫入)。透過使用 @ReadingConverter 註解此轉換器,您指示 Spring Data 轉換資料庫中應分配給 Boolean 屬性的每個 String 值。

將 Spring Converter 註冊到 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,該 bean 將由 Boot 基礎設施拾取。要了解更多資訊,請務必閱讀 Spring Boot 參考文件

JdbcValue

值轉換使用 JdbcValue 來豐富傳播到 JDBC 操作的值,並新增一個 java.sql.Types 型別。如果您需要指定 JDBC 特定的型別而不是使用型別推導,請註冊一個自定義寫入轉換器。此轉換器應將值轉換為 JdbcValue,該型別包含值欄位和實際的 JDBCType 欄位。