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 | 此類別公開了一個 Factory 方法和一個建構子用於物件建立。此處的核心概念是使用 Factory 方法而不是額外的建構子,以避免透過 @PersistenceCreator 進行建構子消除歧義。相反地,屬性的預設值是在 Factory 方法中處理。 |
一般建議
-
盡量堅持使用不可變物件 — 不可變物件的建立非常簡單,因為具體化物件只是一個呼叫其建構子的問題。此外,這還可以防止您的網域物件充斥著允許客戶端程式碼操縱物件狀態的 setter 方法。如果您需要這些方法,最好將它們設為套件保護,以便它們只能由有限數量的同處一地的型別調用。僅使用建構子的具體化比屬性填充快達 30%。
-
提供一個全參數建構子 — 即使您不能或不想將您的實體建模為不可變值,提供一個將實體的所有屬性作為引數的建構子仍然有價值,包括可變屬性,因為這允許物件對應跳過屬性填充以獲得最佳效能。
-
使用 Factory 方法而不是重載的建構子來避免
@PersistenceCreator
— 由於最佳效能需要全引數建構子,我們通常希望公開更多特定於應用程式使用案例的建構子,這些建構子會省略自動產生的識別符等。使用靜態 Factory 方法來公開全參數建構子的這些變體是一種已建立的模式。 -
確保您遵守允許使用產生的實例化器和屬性存取器類別的限制
-
對於要產生的識別符,仍然使用 final 欄位與 wither 方法結合
-
使用 Lombok 來避免樣板程式碼 — 由於持久化操作通常需要一個帶有所有引數的建構子,因此它們的宣告變成了一個乏味的樣板參數到欄位指派的重複,最好透過使用 Lombok 的
@AllArgsConstructor
來避免。
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
。