物件對應基礎知識
本節涵蓋 Spring Data 物件對應、物件建立、欄位和屬性存取、可變性與不可變性的基礎知識。請注意,本節僅適用於不使用底層資料儲存物件對應的 Spring Data 模組(例如 JPA)。另請務必查閱特定儲存區段,以瞭解特定儲存區的物件對應,例如索引、自訂欄或欄位名稱等。
Spring Data 物件對應的核心職責是建立領域物件的實例,並將儲存區原生資料結構對應到這些實例。這表示我們需要兩個基本步驟
-
透過使用公開的建構子之一來建立實例。
-
實例填充以具體化所有公開的屬性。
物件建立
Spring Data 會自動嘗試偵測要用於具體化該類型物件的持久實體建構子。解析演算法如下:
-
如果存在使用
@PersistenceCreator
註解的單一靜態工廠方法,則會使用該方法。 -
如果存在單一建構子,則會使用該建構子。
-
如果存在多個建構子,且只有一個使用
@PersistenceCreator
註解,則會使用該建構子。 -
如果類型是 Java
Record
,則會使用標準建構子。 -
如果存在無引數建構子,則會使用該建構子。其他建構子將會被忽略。
數值解析會假設建構子/工廠方法引數名稱與實體的屬性名稱相符,也就是說,解析的執行方式會如同要填充屬性一般,包括對應中的所有自訂設定(不同的資料儲存欄或欄位名稱等)。這也需要類別檔案中提供參數名稱資訊,或在建構子上存在 @ConstructorProperties
註解。
可以使用 Spring Framework 的 @Value
值註解,並搭配特定儲存區的 SpEL 運算式來自訂數值解析。請參閱關於特定儲存區對應的章節以取得更多詳細資訊。
屬性填充
一旦實體的實例建立完成,Spring Data 就會填充該類別的所有剩餘持久屬性。除非已由實體的建構子填充(亦即透過其建構子引數清單取用),否則識別碼屬性會先被填充,以允許解析循環物件參考。在此之後,所有尚未由建構子填充的非暫時屬性都會設定在實體實例上。為此,我們使用以下演算法
-
如果屬性是不可變的,但公開了
with…
方法(請參閱下方),我們會使用with…
方法來建立具有新屬性值的新實體實例。 -
如果定義了屬性存取(亦即透過 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, this.age);
}
void setRemarks(String remarks) { (5)
this.remarks = remarks;
}
}
1 | 識別碼屬性是 final 的,但在建構子中設定為 null 。此類別公開了 withId(…) 方法,用於設定識別碼,例如,當實例插入資料儲存區且已產生識別碼時。原始的 Person 實例保持不變,因為會建立新的實例。相同的模式通常適用於其他由儲存區管理的屬性,但可能必須為了持久化操作而變更。wither 方法是選用的,因為持久化建構子(請參閱 6)實際上是複製建構子,而設定屬性會轉換為建立套用了新識別碼值的新實例。 |
2 | firstname 和 lastname 屬性是普通的不可變屬性,可能會透過 getter 公開。 |
3 | age 屬性是從 birthday 屬性衍生的不可變屬性。透過顯示的設計,資料庫值將會勝過預設值,因為 Spring Data 使用唯一宣告的建構子。即使意圖是應該優先選擇計算結果,重要的是此建構子也將 age 作為參數(以可能忽略它),否則屬性填充步驟將會嘗試設定 age 欄位,並因其為不可變且沒有 with… 方法而失敗。 |
4 | comment 屬性是可變的,並透過直接設定其欄位來填充。 |
5 | remarks 屬性是可變的,並透過調用 setter 方法來填充。 |
6 | 此類別公開了用於物件建立的工廠方法和建構子。此處的核心概念是使用工廠方法而不是額外的建構子,以避免需要透過 @PersistenceCreator 進行建構子消除歧義。相反地,屬性的預設值是在工廠方法內處理。如果您希望 Spring Data 使用工廠方法進行物件實例化,請使用 @PersistenceCreator 註解它。 |
一般建議
-
嘗試堅持使用不可變物件 — 不可變物件很容易建立,因為具體化物件就只是呼叫其建構子而已。此外,這也避免了您的領域物件充斥著允許用戶端程式碼操作物件狀態的 setter 方法。如果您需要這些方法,最好將它們設為套件保護,以便它們只能由有限數量的共置類型調用。僅限建構子的具體化比屬性填充快最多 30%。
-
提供所有引數建構子 — 即使您無法或不想將實體建模為不可變值,提供一個將實體的所有屬性(包括可變屬性)作為引數的建構子仍然有價值,因為這允許物件對應跳過屬性填充以獲得最佳效能。
-
使用工廠方法而不是多載建構子以避免
@PersistenceCreator
— 由於最佳效能需要所有引數建構子,我們通常希望公開更多應用程式使用案例特定的建構子,這些建構子會省略自動產生的識別碼等。使用靜態工廠方法來公開所有引數建構子的這些變體是一種既定的模式。 -
請確保您遵守允許使用產生的實例化器和屬性存取器類別的限制 —
-
對於要產生的識別碼,仍然使用 final 欄位與所有引數持久化建構子(首選)或
with…
方法結合 — -
使用 Lombok 以避免樣板程式碼 — 由於持久化操作通常需要一個接受所有引數的建構子,因此它們的宣告會變成冗長的重複樣板參數到欄位指派,最好透過使用 Lombok 的
@AllArgsConstructor
來避免。
覆寫屬性
Java 允許領域類別的彈性設計,其中子類別可以定義一個已在其父類別中以相同名稱宣告的屬性。考慮以下範例
public class SuperType {
private CharSequence field;
public SuperType(CharSequence field) {
this.field = field;
}
public CharSequence getField() {
return this.field;
}
public void setField(CharSequence field) {
this.field = field;
}
}
public class SubType extends SuperType {
private String field;
public SubType(String field) {
super(field);
this.field = field;
}
@Override
public String getField() {
return this.field;
}
public void setField(String field) {
this.field = field;
// optional
super.setField(field);
}
}
這兩個類別都使用可指派的類型定義了 field
。但是,SubType
會遮蔽 SuperType.field
。根據類別設計,使用建構子可能是設定 SuperType.field
的唯一預設方法。或者,在 setter 中呼叫 super.setField(…)
可以在 SuperType
中設定 field
。所有這些機制都在某種程度上產生衝突,因為屬性共用相同的名稱,但可能代表兩個不同的值。如果類型不可指派,Spring Data 會跳過父類型屬性。也就是說,覆寫屬性的類型必須可指派給其父類型屬性類型才能註冊為覆寫,否則父類型屬性會被視為暫時性屬性。我們通常建議使用不同的屬性名稱。
Spring Data 模組通常支援保存不同值的覆寫屬性。從程式設計模型角度來看,有幾件事需要考慮
-
應該持久化哪個屬性(預設為所有宣告的屬性)?您可以使用
@Transient
註解這些屬性來排除它們。 -
如何在您的資料儲存區中表示屬性?對不同的值使用相同的欄位/資料行名稱通常會導致資料損壞,因此您應該使用明確的欄位/資料行名稱來註解至少其中一個屬性。
-
@AccessType(PROPERTY)
無法使用,因為父屬性通常無法設定,而無需對 setter 實作進行任何進一步的假設。
Kotlin 支援
Spring Data 調整了 Kotlin 的特性,以允許物件建立和變更。
Kotlin 物件建立
支援實例化 Kotlin 類別,預設情況下所有類別都是不可變的,並且需要明確的屬性宣告來定義可變屬性。
Spring Data 會自動嘗試偵測要用於具體化該類型物件的持久實體建構子。解析演算法如下:
-
如果存在使用
@PersistenceCreator
註解的建構子,則會使用該建構子。 -
如果類型是 Kotlin 資料類別,則會使用主要建構子。
-
如果存在使用
@PersistenceCreator
註解的單一靜態工廠方法,則會使用該方法。 -
如果存在單一建構子,則會使用該建構子。
-
如果存在多個建構子,且只有一個使用
@PersistenceCreator
註解,則會使用該建構子。 -
如果類型是 Java
Record
,則會使用標準建構子。 -
如果存在無引數建構子,則會使用該建構子。其他建構子將會被忽略。
考慮以下 data
類別 Person
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
。
Spring Data 不支援委派屬性。對應中繼資料會篩選 Kotlin 資料類別的委派屬性。在所有其他情況下,您可以透過使用 @delegate:org.springframework.data.annotation.Transient 註解屬性來排除委派屬性的合成欄位。 |
Kotlin 資料類別的屬性填充
在 Kotlin 中,預設情況下所有類別都是不可變的,並且需要明確的屬性宣告來定義可變屬性。考慮以下 data
類別 Person
data class Person(val id: String, val name: String)
此類別實際上是不可變的。它允許建立新實例,因為 Kotlin 會產生一個 copy(…)
方法,該方法會建立新的物件實例,從現有物件複製所有屬性值,並套用作為引數提供給方法的屬性值。
Kotlin 覆寫屬性
Kotlin 允許宣告 屬性覆寫 以變更子類別中的屬性。
open class SuperType(open var field: Int)
class SubType(override var field: Int = 1) :
SuperType(field) {
}
這種安排會呈現兩個名稱為 field
的屬性。Kotlin 會為每個類別中的每個屬性產生屬性存取器(getter 和 setter)。實際上,程式碼看起來如下所示
public class SuperType {
private int field;
public SuperType(int field) {
this.field = field;
}
public int getField() {
return this.field;
}
public void setField(int field) {
this.field = field;
}
}
public final class SubType extends SuperType {
private int field;
public SubType(int field) {
super(field);
this.field = field;
}
public int getField() {
return this.field;
}
public void setField(int field) {
this.field = field;
}
}
SubType
上的 getter 和 setter 僅設定 SubType.field
,而不設定 SuperType.field
。在這種安排中,使用建構子是設定 SuperType.field
的唯一預設方法。在 SubType
中新增一個方法以透過 this.SuperType.field = …
設定 SuperType.field
是可能的,但超出支援的慣例範圍。屬性覆寫會在某種程度上產生衝突,因為屬性共用相同的名稱,但可能代表兩個不同的值。我們通常建議使用不同的屬性名稱。
Spring Data 模組通常支援保存不同值的覆寫屬性。從程式設計模型角度來看,有幾件事需要考慮
-
應該持久化哪個屬性(預設為所有宣告的屬性)?您可以使用
@Transient
註解這些屬性來排除它們。 -
如何在您的資料儲存區中表示屬性?對不同的值使用相同的欄位/資料行名稱通常會導致資料損壞,因此您應該使用明確的欄位/資料行名稱來註解至少其中一個屬性。
-
@AccessType(PROPERTY)
無法使用,因為父屬性無法設定。
Kotlin 值類別
Kotlin 值類別旨在用於更具表現力的領域模型,以明確化底層概念。Spring Data 可以讀取和寫入使用值類別定義屬性的類型。
考慮以下領域模型
@JvmInline
value class EmailAddress(val theAddress: String) (1)
data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) (2)
1 | 具有非可為 null 值類型的簡單值類別。 |
2 | 使用 EmailAddress 值類別定義屬性的資料類別。 |
在編譯後的類別中,使用非原始值類型的非可為 null 屬性會被扁平化為值類型。可為 null 的原始值類型或可為 null 的值中值類型會以其包裝器類型表示,這會影響值類型在資料庫中的表示方式。 |