Spring Data 物件對映基礎
本節涵蓋 Spring Data 物件對映、物件建立、欄位和屬性訪問、可變性和不變性的基礎知識。
Spring Data 物件對映的核心職責是建立領域物件例項,並將儲存原生資料結構對映到這些例項上。這意味著我們需要兩個基本步驟
-
透過使用公開的建構函式之一建立例項。
-
例項填充以具體化所有公開的屬性。
物件建立
Spring Data 會自動嘗試檢測持久化實體的建構函式,該建構函式將用於具體化該型別的物件。解析演算法的工作方式如下
-
如果存在無引數建構函式,則將使用它。其他建構函式將被忽略。
-
如果存在單個帶引數的建構函式,則將使用它。
-
如果存在多個帶引數的建構函式,則 Spring Data 將使用的那個必須用
@PersistenceCreator進行註釋。
值解析假定建構函式引數名與實體的屬性名匹配,即解析將按照屬性將要填充的方式執行,包括對映中的所有自定義(不同的資料儲存列或欄位名等)。這還要求類檔案中提供引數名資訊,或者建構函式上存在 @ConstructorProperties 註解。
屬性填充
一旦建立了實體例項,Spring Data 會填充該類的所有剩餘持久化屬性。除非已由實體的建構函式填充(即透過其建構函式引數列表消耗),否則將首先填充識別符號屬性,以允許解析迴圈物件引用。之後,所有尚未由建構函式填充的非瞬態屬性將在實體例項上設定。為此,我們使用以下演算法
-
如果屬性是不可變的但公開了一個 wither 方法(見下文),我們使用 wither 建立一個具有新屬性值的新實體例項。
-
如果定義了屬性訪問(即透過 getter 和 setter 訪問),我們正在呼叫 setter 方法。
-
預設情況下,我們直接設定欄位值。
讓我們看看以下實體
class Person {
private final @Id Long id; (1)
private final String firstname, lastname; (2)
private final LocalDate birthday;
private final int age; (3)
private String comment; (4)
private @AccessType(Type.PROPERTY) String remarks; (5)
static Person of(String firstname, String lastname, LocalDate birthday) { (6)
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}
Person withId(Long id) { (1)
return new Person(id, this.firstname, this.lastname, this.birthday);
}
void setRemarks(String remarks) { (5)
this.remarks = remarks;
}
}
| 1 | 識別符號屬性是 final 的,但在建構函式中設定為 null。該類公開了一個 withId(…) 方法,用於設定識別符號,例如當一個例項插入到資料儲存中並生成了識別符號時。原始的 Vertex 例項保持不變,因為建立了一個新的例項。同樣的模式通常也適用於其他由儲存管理但可能需要為持久化操作更改的屬性。 |
| 2 | firstname 和 lastname 屬性是普通的不可變屬性,可能透過 getter 公開。 |
| 3 | age 屬性是不可變的,但由 birthday 屬性派生。按照所示設計,資料庫值將優先於預設值,因為 Spring Data 使用唯一宣告的建構函式。即使目的是優先計算,重要的是此建構函式也接受 age 作為引數(可能忽略它),否則屬性填充步驟將嘗試設定 age 欄位,並因其不可變且不存在 wither 而失敗。 |
| 4 | comment 屬性是可變的,並透過直接設定其欄位進行填充。 |
| 5 | remarks 屬性是可變的,透過直接設定 comment 欄位或呼叫 setter 方法進行填充 |
| 6 | 該類公開了一個工廠方法和一個建構函式用於物件建立。這裡的核心思想是使用工廠方法而不是額外的建構函式,以避免透過 @PersistenceCreator 進行建構函式歧義消除。相反,屬性的預設值在工廠方法中處理。 |
一般建議
-
儘量堅持使用不可變物件——不可變物件建立起來很簡單,因為例項化一個物件只需要呼叫其建構函式。此外,這可以防止您的領域物件被散落的 setter 方法汙染,這些方法允許客戶端程式碼操縱物件的內部狀態。如果您需要這些方法,最好將它們設為包保護,以便它們只能由有限數量的同位置型別呼叫。僅透過建構函式進行例項化比透過屬性填充快 30%。
-
提供一個全參建構函式 — 即使您不能或不想將您的實體建模為不可變值,提供一個接受實體所有屬性(包括可變屬性)作為引數的建構函式仍然很有價值,因為這允許物件對映跳過屬性填充以獲得最佳效能。
-
使用工廠方法而不是過載建構函式來避免
@PersistenceCreator—— 為了獲得最佳效能,需要一個全引數建構函式,我們通常希望公開更多針對應用程式用例的建構函式,這些建構函式省略了自動生成的識別符號等。使用靜態工廠方法來公開全引數建構函式的這些變體是一種既定的模式。 -
確保您遵守允許使用生成的例項化器和屬性訪問器類的約束
-
對於要生成的識別符號,仍將 final 欄位與 wither 方法結合使用
-
使用 Lombok 避免樣板程式碼 — 由於持久化操作通常需要一個接受所有引數的建構函式,它們的宣告變成了繁瑣的樣板引數到欄位賦值的重複,最好透過使用 Lombok 的
@AllArgsConstructor來避免。
關於不可變對映的注意事項
儘管我們建議儘可能使用不可變對映和構造,但在對映方面存在一些限制。給定一個雙向關係,其中 A 對 B 有一個建構函式引用,而 B 對 A 有一個引用,或者更復雜的場景。這種雞生蛋/蛋生雞的情況對於 Spring Data Neo4j 來說是無法解決的。在 A 的例項化過程中,它急切地需要一個完全例項化的 B,而 B 反過來又需要 A 的例項(準確地說,是 相同 的例項)。SDN 通常允許此類模型,但如果從資料庫返回的資料包含上述描述的這種結構,則會在執行時丟擲 MappingException。在這種情況下或您無法預測返回的資料是什麼樣子的場景中,您最好為關係使用可變欄位。
Kotlin 支援
Spring Data 適應 Kotlin 的特性,以允許物件建立和修改。
Kotlin 物件建立
支援 Kotlin 類的例項化,所有類預設是不可變的,需要顯式屬性宣告來定義可變屬性。考慮以下 data 類 Vertex
data class Person(val id: String, val name: String)
上面的類編譯成一個帶有顯式建構函式的典型類。我們可以透過新增另一個建構函式並用 @PersistenceCreator 註解它來指示建構函式首選項,從而自定義此類
data class Person(var id: String, val name: String) {
@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}
Kotlin 透過允許在未提供引數時使用預設值來支援引數可選性。當 Spring Data 檢測到帶有引數預設值的建構函式時,如果資料儲存不提供值(或簡單地返回 null),它會保留這些引數,以便 Kotlin 可以應用引數預設值。考慮以下對 name 應用引數預設值的類
data class Person(var id: String, val name: String = "unknown")
每次 name 引數不在結果中或其值為 null 時,name 都會預設為 unknown。