基於元資料的對映
為了充分利用SDN(Spring Data Neo4j)中的物件對映功能,您應該使用@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註解的抽象基類。在此類上定義的標籤將與具體實現的標籤一起用作附加標籤。
我們還在某些場景中支援領域類層次結構中的介面
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.schema.RelationshipId;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
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;
}
public Long getId() {
return id;
}
public void setRelated(SomeInterface related) {
this.related = related;
}
}
| 1 | 使用純介面名稱,就像您命名您的領域一樣 |
| 2 | 由於我們需要同步主標籤,我們將@Node放在實現類上,該類可能在另一個模組中。請注意,值與所實現介面的名稱完全相同。不允許重新命名。 |
使用不同的主標籤而不是介面名稱也是可能的
@Node("PrimaryLabelWN") (1)
public interface SomeInterface2 {
String getName();
SomeInterface2 getRelated();
}
public static class SomeInterfaceEntity2 implements SomeInterface {
// 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 | 未指定具體型別 |
所需的資料結構顯示在以下測試中
void mixedImplementationsRead(@Autowired Neo4jTemplate template) {
Long id;
try (Session session = this.driver.session(this.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 = this.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>。在這種情況下,與其他領域類的關係型別由map的key給出,並且不得透過@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)
private List<Roles> actorsAndRoles = new ArrayList<>();
一個完整的例子
將所有這些放在一起,我們可以建立一個簡單的領域模型。我們使用具有不同角色的電影和人物
MovieEntityimport 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。領域類是相同的
PersonEntityimport 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 this.born;
}
public String getName() {
return this.name;
}
}
我們沒有雙向建模電影和人物之間的關係。這是為什麼呢?我們將MovieEntity視為聚合根,擁有這些關係。另一方面,我們希望能夠從資料庫中提取所有人,而無需選擇與他們相關聯的所有電影。請在嘗試雙向對映資料庫中的每個關係之前,考慮您的應用程式用例。雖然您可以這樣做,但您最終可能會在物件圖中重建一個圖資料庫,這不是對映框架的意圖。如果您必須建模您的迴圈或雙向領域模型,並且不想獲取整個圖,您可以透過使用投影來定義您想要獲取的資料的細粒度描述。 |