基於元數據的映射
為了充分利用 SDN 內部的物件映射功能,您應該使用 @Node
註解標註您的映射物件。雖然映射框架並非必須使用此註解(即使沒有任何註解,您的 POJO 也能正確映射),但它能讓類別路徑掃描器找到並預處理您的領域物件,以提取必要的元數據。如果您不使用此註解,您的應用程式在第一次儲存領域物件時會稍微降低效能,因為映射框架需要建立其內部元數據模型,以便了解您的領域物件的屬性以及如何持久化它們。
映射註解概觀
來自 SDN
-
@Node
:應用於類別層級,表示此類別是映射到資料庫的候選者。 -
@Id
:應用於欄位層級,標記用於識別目的的欄位。 -
@GeneratedValue
:與@Id
一起應用於欄位層級,以指定應如何產生唯一識別符。 -
@Property
:應用於欄位層級,以修改從屬性到屬性的映射。 -
@CompositeProperty
:應用於欄位層級,針對應作為複合屬性讀回的 Map 類型的屬性。請參閱〈複合屬性〉。 -
@Relationship
:應用於欄位層級,以指定關聯的詳細資訊。 -
@DynamicLabels
:應用於欄位層級,以指定動態標籤的來源。 -
@RelationshipProperties
:應用於類別層級,表示此類別作為關聯屬性的目標。 -
@TargetNode
:應用於使用@RelationshipProperties
註解的類別的欄位,以從另一端的角度標記該關聯的目標。
以下註解用於指定轉換並確保與 OGM 的向後相容性。
-
@DateLong
-
@DateString
-
@ConvertWith
請參閱〈轉換〉以取得更多相關資訊。
來自 Spring Data Commons
-
@org.springframework.data.annotation.Id
與 SDN 的@Id
相同,事實上,@Id
是使用 Spring Data Common 的 Id 註解進行註解。 -
@CreatedBy
:應用於欄位層級,表示節點的建立者。 -
@CreatedDate
:應用於欄位層級,表示節點的建立日期。 -
@LastModifiedBy
:應用於欄位層級,表示節點最後變更的作者。 -
@LastModifiedDate
:應用於欄位層級,表示節點的最後修改日期。 -
@PersistenceCreator
:應用於一個建構子,以將其標記為讀取實體時的首選建構子。 -
@Persistent
:應用於類別層級,表示此類別是映射到資料庫的候選者。 -
@Version
:應用於欄位層級,用於樂觀鎖定,並在儲存操作時檢查修改。初始值為零,每次更新時會自動遞增。 -
@ReadOnlyProperty
:應用於欄位層級,以將屬性標記為唯讀。屬性將在資料庫讀取期間被填充,但不會受到寫入的影響。在關聯中使用時,請注意,如果沒有以其他方式關聯,則該集合中沒有相關的實體會被持久化。
請參閱〈稽核〉以了解所有關於稽核支援的註解。
基本建構區塊:@Node
@Node
註解用於將類別標記為受管理的領域類別,受映射上下文的類別路徑掃描約束。
為了將物件映射到圖形中的節點,反之亦然,我們需要一個標籤來識別要映射的類別以及來源。
@Node
具有 labels
屬性,可讓您設定一個或多個標籤,用於讀取和寫入已註解類別的實例。value
屬性是 labels
的別名。如果您未指定標籤,則將使用簡單類別名稱作為主要標籤。如果您想提供多個標籤,您可以:
-
將陣列提供給
labels
屬性。陣列中的第一個元素將被視為主要標籤。 -
為
primaryLabel
提供一個值,並將額外的標籤放在labels
中。
主要標籤應始終是最能反映您領域類別的具體標籤。
對於透過儲存庫或 Neo4j 範本寫入的已註解類別的每個實例,圖形中至少帶有主要標籤的一個節點將被寫入。反之亦然,所有帶有主要標籤的節點都將映射到已註解類別的實例。
關於類別階層的注意事項
@Node
註解不會從超類型和介面繼承。但是,您可以個別在每個繼承層級註解您的領域類別。這允許多型查詢:您可以傳入基本或中間類別,並檢索節點的正確具體實例。這僅適用於使用 @Node
註解的抽象基底類別。在此類別上定義的標籤將與具體實作的標籤一起用作額外標籤。
我們在某些情況下也支援領域類別階層中的介面
public interface SomeInterface { (1)
String getName();
SomeInterface getRelated();
}
@Node("SomeInterface") (2)
public static class SomeInterfaceEntity implements SomeInterface {
@Id
@GeneratedValue
private Long id;
private final String name;
private SomeInterface related;
public SomeInterfaceEntity(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public SomeInterface getRelated() {
return related;
}
}
1 | 僅使用純介面名稱,就像您命名您的領域一樣 |
2 | 由於我們需要同步主要標籤,因此我們將 @Node 放在實作類別上,該類別可能位於另一個模組中。請注意,該值與實作的介面名稱完全相同。無法重新命名。 |
也可以使用與介面名稱不同的主要標籤
@Node("PrimaryLabelWN") (1)
public interface SomeInterface2 {
String getName();
SomeInterface2 getRelated();
}
public static class SomeInterfaceEntity2 implements SomeInterface2 {
// Overrides omitted for brevity
}
1 | 將 @Node 註解放在介面上 |
也可以使用介面的不同實作並擁有一個多型領域模型。這樣做時,至少需要兩個標籤:一個標籤確定介面,另一個標籤確定具體類別
@Node("SomeInterface3") (1)
public interface SomeInterface3 {
String getName();
SomeInterface3 getRelated();
}
@Node("SomeInterface3a") (2)
public static class SomeInterfaceImpl3a implements SomeInterface3 {
// Overrides omitted for brevity
}
@Node("SomeInterface3b") (3)
public static class SomeInterfaceImpl3b implements SomeInterface3 {
// Overrides omitted for brevity
}
@Node
public static class ParentModel { (4)
@Id
@GeneratedValue
private Long id;
private SomeInterface3 related1; (5)
private SomeInterface3 related2;
}
1 | 在這種情況下,需要明確指定識別介面的標籤 |
2 | 這適用於第一個… |
3 | 以及第二個實作 |
4 | 這是一個用戶端或父模型,透明地使用 SomeInterface3 進行兩個關聯 |
5 | 未指定具體類型 |
以下測試顯示了所需的資料結構。OGM 也會寫入相同的結構
Long id;
try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) {
id = transaction.run("" +
"CREATE (s:ParentModel{name:'s'}) " +
"CREATE (s)-[:RELATED_1]-> (:SomeInterface3:SomeInterface3b {name:'3b'}) " +
"CREATE (s)-[:RELATED_2]-> (:SomeInterface3:SomeInterface3a {name:'3a'}) " +
"RETURN id(s)")
.single().get(0).asLong();
transaction.commit();
}
Optional<Inheritance.ParentModel> optionalParentModel = transactionTemplate.execute(tx ->
template.findById(id, Inheritance.ParentModel.class));
assertThat(optionalParentModel).hasValueSatisfying(v -> {
assertThat(v.getName()).isEqualTo("s");
assertThat(v).extracting(Inheritance.ParentModel::getRelated1)
.isInstanceOf(Inheritance.SomeInterfaceImpl3b.class)
.extracting(Inheritance.SomeInterface3::getName)
.isEqualTo("3b");
assertThat(v).extracting(Inheritance.ParentModel::getRelated2)
.isInstanceOf(Inheritance.SomeInterfaceImpl3a.class)
.extracting(Inheritance.SomeInterface3::getName)
.isEqualTo("3a");
});
介面不能定義識別符欄位。因此,它們不是儲存庫的有效實體類型。 |
動態或「執行階段」管理的標籤
所有透過簡單類別名稱隱式定義或透過 @Node
註解顯式定義的標籤都是靜態的。它們無法在執行階段變更。如果您需要可以在執行階段操作的額外標籤,您可以使用 @DynamicLabels
。@DynamicLabels
是欄位層級的註解,將 java.util.Collection<String>
類型(例如 List
或 Set
)的屬性標記為動態標籤的來源。
如果存在此註解,則在載入期間,節點上存在的所有未透過 @Node
和類別名稱靜態映射的標籤都將收集到該集合中。在寫入期間,節點的所有標籤都將替換為靜態定義的標籤加上集合的內容。
如果您的其他應用程式將額外標籤新增至節點,請勿使用 @DynamicLabels 。如果受管理的實體上存在 @DynamicLabels ,則產生的標籤集將是寫入資料庫的「真相」。 |
識別實例:@Id
雖然 @Node
在類別和具有特定標籤的節點之間建立映射,但我們也需要在該類別的個別實例(物件)與節點的實例之間建立連線。
這就是 @Id
發揮作用的地方。@Id
將類別的屬性標記為物件的唯一識別符。在理想世界中,該唯一識別符是一個唯一的業務鍵,或者換句話說,是一個自然鍵。@Id
可用於所有具有支援的簡單類型的屬性。
然而,自然鍵很難找到。例如,人名很少是唯一的,會隨時間變化,或者更糟的是,並非每個人都有名字和姓氏。
因此,我們支援兩種不同的*代理鍵*。
在 String
、long
或 Long
類型的屬性上,@Id
可以與 @GeneratedValue
一起使用。Long
和 long
映射到 Neo4j 內部 ID。String
映射到自 Neo4j 5 以來可用的 *elementId*。兩者**都不是**節點或關聯的屬性,通常不可見,對於屬性,並允許 SDN 檢索類別的個別實例。
@GeneratedValue
提供 generatorClass
屬性。generatorClass
可用於指定實作 IdGenerator
的類別。IdGenerator
是一個函數介面,其 generateId
接受主要標籤和要為其產生 ID 的實例。我們支援 UUIDStringGenerator
作為開箱即用的實作之一。
您也可以透過 generatorRef
在 @GeneratedValue
上指定應用程式上下文中的 Spring Bean。該 Bean 也需要實作 IdGenerator
,但可以利用上下文中的所有內容,包括 Neo4j 用戶端或範本來與資料庫互動。
不要跳過〈處理和配置唯一 ID〉中關於 ID 處理的重要注意事項 |
樂觀鎖定:@Version
Spring Data Neo4j 透過在 Long
類型欄位上使用 @Version
註解來支援樂觀鎖定。此屬性將在更新期間自動遞增,並且不得手動修改。
例如,如果不同執行緒中的兩個交易想要修改版本為 x
的相同物件,則第一個操作將成功持久化到資料庫。此時,版本欄位將遞增,因此變為 x+1
。第二個操作將失敗,並出現 OptimisticLockingFailureException
,因為它想要修改版本為 x
的物件,而該版本已不再存在於資料庫中。在這種情況下,需要重試操作,從從資料庫中重新提取具有目前版本的物件開始。
如果使用業務 ID,@Version
屬性也是強制性的。Spring Data Neo4j 將檢查此欄位,以判斷實體是新的還是已持久化。
映射屬性:@Property
使用 @Node
註解的類別的所有屬性都將持久化為 Neo4j 節點和關聯的屬性。在沒有進一步設定的情況下,Java 或 Kotlin 類別中屬性的名稱將用作 Neo4j 屬性。
如果您正在使用現有的 Neo4j 結構描述,或者只是想根據您的需求調整映射,則需要使用 @Property
。name
用於指定資料庫中屬性的名稱。
連接節點:@Relationship
@Relationship
註解可用於所有非簡單類型的屬性。它適用於使用 @Node
或其集合和映射註解的其他類型屬性。
type
或 value
屬性允許設定關聯的類型,direction
允許指定方向。SDN 中的預設方向是 Relationship.Direction#OUTGOING
。
我們支援動態關聯。動態關聯表示為 Map<String, AnnotatedDomainClass>
或 Map<Enum, AnnotatedDomainClass>
。在這種情況下,與其他領域類別的關聯類型由映射的鍵給定,並且不得透過 @Relationship
設定。
映射關聯屬性
Neo4j 支援不僅在節點上定義屬性,而且在關聯上也定義屬性。為了在模型中表達這些屬性,SDN 提供了 @RelationshipProperties
,可用於簡單的 Java 類別。在屬性類別中,必須恰好有一個欄位標記為 @TargetNode
,以定義關聯指向的實體。或者,在 INCOMING
關聯上下文中,是來自哪個實體。
關聯屬性類別及其用法可能如下所示
Roles
@RelationshipProperties
public class Roles {
@RelationshipId
private Long id;
private final List<String> roles;
@TargetNode
private final PersonEntity person;
public Roles(PersonEntity person, List<String> roles) {
this.person = person;
this.roles = roles;
}
public List<String> getRoles() {
return roles;
}
@Override
public String toString() {
return "Roles{" +
"id=" + id +
'}' + this.hashCode();
}
}
您必須為產生的內部 ID (@RelationshipId
) 定義一個屬性,以便 SDN 可以在儲存期間確定哪些關聯可以安全地覆寫而不會遺失屬性。如果 SDN 找不到用於儲存內部節點 ID 的欄位,則啟動期間將會失敗。
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) (1)
private List<Roles> actorsAndRoles = new ArrayList<>();
完整範例
將所有這些放在一起,我們可以建立一個簡單的領域。我們使用電影和具有不同角色的人員
MovieEntity
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.schema.Relationship.Direction;
@Node("Movie") (1)
public class MovieEntity {
@Id (2)
private final String title;
@Property("tagline") (3)
private final String description;
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) (4)
private List<Roles> actorsAndRoles = new ArrayList<>();
@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();
public MovieEntity(String title, String description) { (5)
this.title = title;
this.description = description;
}
// Getters omitted for brevity
}
1 | @Node 用於將此類別標記為受管理的實體。它也用於設定 Neo4j 標籤。如果您只是使用普通的 @Node ,則標籤預設為類別的名稱。 |
2 | 每個實體都必須有一個 ID。我們使用電影的名稱作為唯一識別符。 |
3 | 這顯示了 @Property 作為一種為欄位使用與圖形屬性不同名稱的方法。 |
4 | 這設定與人員的傳入關聯。 |
5 | 這是您的應用程式碼以及 SDN 要使用的建構子。 |
人員在這裡映射到兩個角色:actors
和 directors
。領域類別相同
PersonEntity
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
@Node("Person")
public class PersonEntity {
@Id private final String name;
private final Integer born;
public PersonEntity(Integer born, String name) {
this.born = born;
this.name = name;
}
public Integer getBorn() {
return born;
}
public String getName() {
return name;
}
}
我們尚未在兩個方向上對電影和人員之間的關聯進行建模。為什麼?我們將 MovieEntity 視為聚合根,擁有關聯。另一方面,我們希望能夠從資料庫中提取所有人員,而無需選擇與他們關聯的所有電影。在您嘗試在每個方向上映射資料庫中的每個關聯之前,請考慮您的應用程式的使用案例。雖然您可以這樣做,但您最終可能會在您的物件圖形內部重建圖形資料庫,而這並不是映射框架的意圖。如果您必須對您的循環或雙向領域進行建模,並且不想提取整個圖形,則可以使用投影來定義您想要提取的資料的細粒度描述。 |