基於元數據的映射

為了充分利用 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 的別名。如果您未指定標籤,則將使用簡單類別名稱作為主要標籤。如果您想提供多個標籤,您可以:

  1. 將陣列提供給 labels 屬性。陣列中的第一個元素將被視為主要標籤。

  2. 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> 類型(例如 ListSet)的屬性標記為動態標籤的來源。

如果存在此註解,則在載入期間,節點上存在的所有未透過 @Node 和類別名稱靜態映射的標籤都將收集到該集合中。在寫入期間,節點的所有標籤都將替換為靜態定義的標籤加上集合的內容。

如果您的其他應用程式將額外標籤新增至節點,請勿使用 @DynamicLabels。如果受管理的實體上存在 @DynamicLabels,則產生的標籤集將是寫入資料庫的「真相」。

識別實例:@Id

雖然 @Node 在類別和具有特定標籤的節點之間建立映射,但我們也需要在該類別的個別實例(物件)與節點的實例之間建立連線。

這就是 @Id 發揮作用的地方。@Id 將類別的屬性標記為物件的唯一識別符。在理想世界中,該唯一識別符是一個唯一的業務鍵,或者換句話說,是一個自然鍵。@Id 可用於所有具有支援的簡單類型的屬性。

然而,自然鍵很難找到。例如,人名很少是唯一的,會隨時間變化,或者更糟的是,並非每個人都有名字和姓氏。

因此,我們支援兩種不同的*代理鍵*。

StringlongLong 類型的屬性上,@Id 可以與 @GeneratedValue 一起使用。Longlong 映射到 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 結構描述,或者只是想根據您的需求調整映射,則需要使用 @Propertyname 用於指定資料庫中屬性的名稱。

連接節點:@Relationship

@Relationship 註解可用於所有非簡單類型的屬性。它適用於使用 @Node 或其集合和映射註解的其他類型屬性。

typevalue 屬性允許設定關聯的類型,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<>();

關聯查詢備註

一般來說,建立查詢的關聯/跳數沒有限制。SDN 會從您建模的節點解析整個可到達的圖形。

話雖如此,當有雙向映射關聯的想法時,也就是說,您在實體的兩端都定義了關聯,您可能會得到超出您預期的結果。

考慮一個範例,其中一部*電影*有*演員*,並且您想要提取某部電影及其所有演員。如果從*電影*到*演員*的關聯只是單向的,這將不會有問題。在雙向情境中,SDN 將提取特定的*電影*、其*演員*,以及根據關聯的定義為此*演員*定義的其他電影。在最壞的情況下,這將級聯到為單一實體提取整個圖形。

完整範例

將所有這些放在一起,我們可以建立一個簡單的領域。我們使用電影和具有不同角色的人員

範例 1. 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 要使用的建構子。

人員在這裡映射到兩個角色:actorsdirectors。領域類別相同

範例 2. 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 視為聚合根,擁有關聯。另一方面,我們希望能夠從資料庫中提取所有人員,而無需選擇與他們關聯的所有電影。在您嘗試在每個方向上映射資料庫中的每個關聯之前,請考慮您的應用程式的使用案例。雖然您可以這樣做,但您最終可能會在您的物件圖形內部重建圖形資料庫,而這並不是映射框架的意圖。如果您必須對您的循環或雙向領域進行建模,並且不想提取整個圖形,則可以使用投影來定義您想要提取的資料的細粒度描述。