基於元資料的對映

為了充分利用 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 中。

主標籤應始終是反映您的域類的最具體的標籤。

對於透過 repository 或 Neo4j template 寫入的每個帶有註解的類的例項,圖資料庫中將至少寫入一個帶有主標籤的節點。反之,所有帶有主標籤的節點都將對映到帶有註解的類的例項。

關於類層次結構的注意事項

@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");
});
介面無法定義識別符號欄位。因此,它們不是 repositories 的有效實體型別。

動態或“執行時”管理的標籤

所有透過簡單類名隱式定義或透過 @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 提供屬性 generatorClassgeneratorClass 可用於指定實現 IdGenerator 的類。IdGenerator 是一個函式式介面,其 generateId 方法接受主標籤和要生成 ID 的例項。我們開箱即用地支援 UUIDStringGenerator 作為一種實現。

您還可以透過 @GeneratedValuegeneratorRef 指定應用上下文中的 Spring Bean。該 bean 也需要實現 IdGenerator,但可以使用上下文中的所有內容,包括 Neo4j client 或 template 來與資料庫互動。

不要跳過關於 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 schema 或只是想根據您的需求調整對映,則需要使用 @Propertyname 屬性用於指定資料庫中屬性的名稱。

連線節點:@Relationship

@Relationship 註解可用於所有非簡單型別的屬性。它適用於用 @Node 註解的其他型別的屬性,或它們的集合和對映。

typevalue 屬性允許配置關係的型別,direction 屬性允許指定方向。SDN 中的預設方向是 Relationship.Direction#OUTGOING

我們支援動態關係。動態關係表示為 Map<String, AnnotatedDomainClass>Map<Enum, AnnotatedDomainClass>。在這種情況下,與其他域類的關係型別由 Map 的鍵給出,並且不得透過 @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 會根據關係定義獲取特定的 *電影*、其 *演員*,以及為此 *演員* 定義的其他電影。在最壞的情況下,這會級聯獲取單個實體的整個圖。

如果您必須建模迴圈或雙向域且不想獲取整個圖,您可以使用projection(投影)來定義您想要獲取的資料的細粒度描述。

一個完整示例

將所有這些結合起來,我們可以建立一個簡單的域。我們使用具有不同角色的電影和人物
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 示例 1. MovieEntity
2 @Node 用於將此類標記為託管實體。它也用於配置 Neo4j 標籤。如果您只使用簡單的 @Node,標籤預設採用類名。
3 每個實體都必須有一個 ID。我們使用電影名稱作為唯一識別符號。
4 這展示了 @Property 如何用於為欄位使用與圖屬性不同的名稱。
5 這配置了一個到人物的傳入關係。

這是您的應用程式程式碼和 SDN 都可以使用的建構函式。

這裡的人物以兩種角色對映:actors(演員)和 directors(導演)。域類是相同的
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;
	}

}
示例 2. PersonEntity