基于元数据映射
为了充分利用 SDN 中的对象映射功能,您应该使用 @Node
注解来注解您的映射对象。尽管映射框架不需要此注解(即使没有任何注解,您的 POJO 也会被正确映射),但它可以让类路径扫描器找到并预处理您的域对象以提取必要的元数据。如果您不使用此注解,则您的应用程序在第一次存储域对象时会略微降低性能,因为映射框架需要构建其内部元数据模型,以便它了解域对象的属性以及如何持久化它们。
映射注解概述
来自 SDN
-
@Node
:应用于类级别,表示此类是映射到数据库的候选类。 -
@Id
:应用于字段级别,用于标记用于标识目的的字段。 -
@GeneratedValue
:与@Id
一起应用于字段级别,用于指定如何生成唯一标识符。 -
@Property
:应用于字段级别,用于修改从属性到属性的映射。 -
@CompositeProperty
:应用于 Map 类型属性的字段级别,该属性应作为复合属性读取。请参阅 复合属性。 -
@Relationship
:应用于字段级别,用于指定关系的详细信息。 -
@DynamicLabels
:应用于字段级别,用于指定动态标签的来源。 -
@RelationshipProperties
:应用于类级别,表示此类作为关系属性的目标。 -
@TargetNode
:应用于用@RelationshipProperties
注解的类的字段,用于从另一端角度标记该关系的目标。
以下注解用于指定转换并确保与 OGM 的向后兼容性。
-
@DateLong
-
@DateString
-
@ConvertWith
有关详细信息,请参阅 转换。
来自 Spring Data 通用注解
-
@org.springframework.data.annotation.Id
与 SDN 中的@Id
相同,实际上,@Id
使用了 Spring Data 通用注解的 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
作为一种实现。
您还可以通过 @GeneratedValue
上的 generatorRef
指定应用程序上下文中 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 视为聚合根,拥有这些关系。另一方面,我们希望能够从数据库中提取所有人员,而无需选择与他们关联的所有电影。在尝试在数据库中双向映射所有关系之前,请考虑应用程序的使用场景。虽然可以这样做,但最终可能会在对象图中重建一个图数据库,这不是映射框架的意图。如果必须建模循环或双向域并且不想获取整个图,则可以使用投影定义要获取的数据的细粒度描述。 |