對映
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等) -
列舉對映到它們的名稱。
-
字串 -
java.util.Date、java.time.LocalDate、java.time.LocalDateTime和java.time.LocalTime -
如果您的資料庫支援陣列型別,則上述型別的陣列和集合可以對映到陣列型別的列。
-
您的資料庫驅動程式接受的任何型別。
-
對其他實體的引用。它們被視為一對一關係或嵌入型別。對於一對一關係實體,
id屬性是可選的。被引用實體的表預計有一個額外的列,其名稱基於引用實體,請參閱反向引用。嵌入實體不需要id。如果存在,它將作為普通屬性對映,沒有任何特殊含義。 -
Set<some entity>被視為一對多關係。被引用實體的表預計有一個額外的列,其名稱基於引用實體,請參閱反向引用。 -
Map<simple type, some entity>被視為限定的一對多關係。被引用實體的表預計有兩個額外的列:一個基於引用實體用於外部索引鍵(參見反向引用),另一個具有相同的名稱和額外的_key字尾用於 map 鍵。 -
List<some entity>對映為Map<Integer, some entity>。需要相同的附加列,並且使用的名稱可以以相同的方式進行自定義。對於
List、Set和Map,反向引用的命名可以透過分別實現NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner)和NamingStrategy.getKeyColumn(RelationalPersistentProperty property)來控制。另外,您可以使用@MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name")註解屬性。為Set指定鍵列沒有效果。 -
您為其註冊了合適的自定義轉換器的型別。
對映註解概述
RelationalConverter 可以使用元資料來驅動物件到行的對映。以下註解可用
-
@Embedded:帶有此註解的屬性將被對映到父實體的表,而不是單獨的表。允許指定結果列是否應具有共同的字首。如果此類實體導致的所有列都為null,則根據@Embedded.onEmpty()的值,被註解的實體將為null或“空”,即其所有屬性都將為null。可以與@Id結合形成複合 id。 -
@Id:應用於欄位級別以標記主鍵。它可以與@Embedded結合形成複合 id。 -
@InsertOnlyProperty:將屬性標記為僅在插入期間寫入。聚合根上的此類屬性將只寫入一次,並且永遠不會更新。請注意,在巢狀實體上,所有儲存操作都會導致插入,因此此註解對巢狀實體的屬性沒有影響。 -
@MappedCollection:允許配置集合或單個巢狀實體如何對映。idColumn指定用於引用父實體主鍵的列。keyColumn指定用於儲存List的索引或Map的鍵的列。 -
@Sequence:為生成註解屬性的值指定資料庫序列。 -
@Table:應用於類級別,表示此類是對映到資料庫的候選。您可以指定儲存資料庫的表的名稱。 -
@Transient:預設情況下,所有欄位都對映到行。此註解將應用於它的欄位從資料庫儲存中排除。瞬態屬性不能在持久化建構函式中使用,因為轉換器無法為建構函式引數例項化值。 -
@PersistenceCreator:標記給定的建構函式或靜態工廠方法——甚至是包保護的——在從資料庫例項化物件時使用。建構函式引數按名稱對映到檢索行中的值。 -
@Value:此註解是 Spring Framework 的一部分。在對映框架中,它可以應用於建構函式引數。這允許您使用 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 值的包裝器,它將該值標記為對不同聚合的引用。此外,該聚合的型別在型別引數中編碼。
反向引用
聚合中的所有引用都會導致資料庫中相反方向的外部索引鍵關係。預設情況下,外部索引鍵列的名稱是引用實體的表名。
如果被引用的 ID 是 @Embedded ID,則反向引用由多個列組成,每個列的名稱由 <table-name> + _ + <column-name> 拼接而成。例如,對一個 Person 實體(具有包含 firstName 和 lastName 屬性的複合 ID)的反向引用將由兩個列 PERSON_FIRST_NAME 和 PERSON_LAST_NAME 組成。
另外,您可以選擇根據引用實體的實體名稱來命名它們,忽略 @Table 註解。您可以透過在 RelationalMappingContext 上呼叫 setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING) 來啟用此行為。
對於 List 和 Map 引用,需要一個額外的列來儲存列表索引或 map 鍵。它基於外部索引鍵列,並帶有額外的 _KEY 字尾。
如果您需要完全不同的命名這些反向引用的方式,您可以按照自己的需求實現 NamingStrategy.getReverseColumnName(RelationalPersistentEntity<?> owner)。
AggregateReferenceclass Person {
@Id long id;
AggregateReference<Person, Long> bestFriend;
}
// ...
Person p1, p2 = // some initialization
p1.bestFriend = AggregateReference.to(p2.id);
您不應該在實體中包含屬性來儲存反向引用的實際值,也不應儲存 map 或 list 的鍵列。如果您希望這些值在您的領域模型中可用,我們建議在 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 註解可用於引用型別(一對一關係)或 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 時將物件設定為 null。
與此行為相反,USE_EMPTY 嘗試使用預設建構函式或接受結果集中可空引數值的建構函式建立新例項。
class MyEntity {
@Id
Integer id;
@Embedded(onEmpty = USE_NULL) (1)
EmbeddedEntity embeddedEntity;
}
class EmbeddedEntity {
String name;
}
| 1 | 如果 name 為 null,則 embeddedEntity 為 null。使用 USE_EMPTY 例項化 embeddedEntity,其中 name 屬性可能為 null。 |
如果您在實體中需要多次使用值物件,這可以透過 @Embedded 註解的可選 prefix 元素實現。此元素表示一個字首,並預先新增到嵌入物件中的每個列名。
|
使用快捷方式
|
包含 Collection 或 Map 的嵌入式實體將始終被視為非空,因為它們至少會包含空集合或 map。因此,即使使用 @Embedded(onEmpty = USE_NULL),此類實體也永遠不會為 null。
嵌入式 ID
識別符號屬性可以使用 @Embedded 註解,從而允許使用複合 ID。完整的嵌入式實體被視為 ID,因此判斷聚合是否被視為需要插入的新聚合或需要更新的現有聚合的檢查是基於該實體,而不是其元素。大多數用例將需要自定義 BeforeConvertCallback 來為新的聚合設定 ID。
@Table("PERSON_WITH_COMPOSITE_ID")
record Person( (1)
@Id @Embedded.Nullable Name pk, (2)
String nickName,
Integer age
) {
}
record Name(String first, String last) {
}
CREATE TABLE PERSON_WITH_COMPOSITE_ID (
FIRST VARCHAR(100),
LAST VARCHAR(100),
NICK_NAME VARCHAR(100),
AGE INT,
PRIMARY KEY (FIRST, LAST) (3)
);
| 1 | 實體可以表示為記錄,無需任何特殊考慮。 |
| 2 | pk 被標記為 id 並嵌入 |
| 3 | 嵌入式 Name 實體中的兩個列構成了資料庫中的主鍵。 |
表建立的詳細資訊取決於所使用的資料庫。
只讀屬性
使用 @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 值進行轉換。
將 Spring 轉換器註冊到 JdbcConverter
class MyJdbcConfiguration extends AbstractJdbcConfiguration {
// …
@Override
protected List<?> userConverters() {
return Arrays.asList(new BooleanToStringConverter(), new StringToBooleanConverter());
}
}
在 Spring Data JDBC 的早期版本中,建議直接覆蓋 AbstractJdbcConfiguration.jdbcCustomConversions()。現在不再需要甚至不推薦這樣做,因為該方法彙集了適用於所有資料庫的轉換、使用的 JdbcDialect 註冊的轉換以及使用者註冊的轉換。如果您正在從舊版本的 Spring Data JDBC 遷移,並且已經覆蓋了 AbstractJdbcConfiguration.jdbcCustomConversions(),則您的 JdbcDialect 中的轉換將不會被註冊。 |
|
如果您想依賴 Spring Boot 來啟動 Spring Data JDBC,但仍想覆蓋配置的某些方面,您可能希望公開該型別的 bean。對於自定義轉換,您可以選擇註冊一個 |