此版本仍在开发中,尚未被视为稳定版本。如需最新的稳定版本,请使用 Spring Data Neo4j 7.4.0! |
基于元数据的映射
要充分利用 SDN 中的对象映射功能,您应该使用注释对映射的对象进行注释。
尽管 Map 框架不需要具有此 Comments(即使没有任何 Comments,您的 POJO 也被正确映射),但它允许 Classpath 扫描器查找并预处理您的域对象以提取必要的元数据。
如果不使用此注释,则应用程序在首次存储域对象时的性能会受到轻微影响,因为映射框架需要构建其内部元数据模型,以便它了解域对象的属性以及如何持久保存它们。@Node
映射注释概述
从 SDN
-
@Node
:在类级别应用,以指示此类是映射到数据库的候选项。 -
@Id
:在字段级别应用,以标记用于身份目的的字段。 -
@GeneratedValue
:在字段级别应用,以指定应如何生成唯一标识符。@Id
-
@Property
:在字段级别应用,以修改从属性到属性的映射。 -
@CompositeProperty
:在字段级别应用于 Map 类型的属性,该属性应作为复合读回。请参阅复合属性。 -
@Relationship
:在字段级别应用以指定关系的详细信息。 -
@DynamicLabels
:在字段级别应用以指定动态标签的来源。 -
@RelationshipProperties
:在类级别应用,以指示此类作为关系属性的目标。 -
@TargetNode
:应用于批注的类的字段,以从另一端的角度标记该关系的目标。@RelationshipProperties
以下注释用于指定转换并确保与 OGM 的向后兼容性。
-
@DateLong
-
@DateString
-
@ConvertWith
有关更多信息,请参阅 Conversions 。
来自 Spring Data commons
-
@org.springframework.data.annotation.Id
与 SDN 相同,实际上使用 Spring Data Common 的 Id 注释进行注释。@Id
@Id
-
@CreatedBy
:在字段级别应用,以指示节点的创建者。 -
@CreatedDate
:在字段级别应用,以指示节点的创建日期。 -
@LastModifiedBy
:在字段级别应用,以指示对节点的上次更改的作者。 -
@LastModifiedDate
:在字段级别应用,以指示节点的上次修改日期。 -
@PersistenceCreator
:应用于一个构造函数,以在读取实体时将其标记为首选构造函数。 -
@Persistent
:在类级别应用,以指示此类是映射到数据库的候选项。 -
@Version
:应用于字段级别,用于乐观锁定,并检查保存操作的修改。 初始值为 0,每次更新时都会自动递增。 -
@ReadOnlyProperty
:在字段级别应用以将属性标记为只读。该属性将在数据库读取期间被水合, 但不受写入的约束。当用于关系时,请注意,该集合中的任何相关实体都不会被保留 如果没有其他相关。
请查看 审计 ,了解有关审计支持的所有注释。
基本构建块:@Node
该注释用于将类标记为托管域类,并受映射上下文的类路径扫描。@Node
要将 Object 映射到图中的节点(反之亦然),我们需要一个标签来标识要映射到和从的类。
@Node
具有一个属性,该属性允许您配置一个或多个标签,以便在读取和写入 Annotated 类的实例时使用。
该属性是 的别名。
如果未指定标签,则简单类名将用作主标签。
如果要提供多个标签,则可以:labels
value
labels
-
为属性提供数组。 数组中的第一个元素将被视为主标签。
labels
-
为 提供一个值,并将其他标签放入 中。
primaryLabel
labels
主标签应始终是反映您的域类的最具体标签。
对于通过存储库或 Neo4j 模板编写的带注释类的每个实例,将写入图中至少具有主标签的一个节点。 反之亦然,所有具有 primary 标签的节点都将映射到带 Comments 的类的实例。
关于类层次结构的说明
注解不是从超类型和接口继承的。
但是,您可以在每个继承级别单独注释域类。
这允许多态查询:您可以传入基类或中间类,并为您的节点检索正确的具体实例。
这仅支持用 .
在此类上定义的标签将与具体实现的标签一起用作附加标签。@Node
@Node
在某些情况下,我们还支持 domain-class-hierarchies 中的接口:
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 | 由于我们需要同步主标签,因此我们放置了 implementation 类,该类
可能在另一个模块中。请注意,该值与接口的名称完全相同
实现。无法重命名。@Node |
也可以使用不同的主标签而不是接口名称:
@Node("PrimaryLabelWN") (1)
public interface SomeInterface2 {
String getName();
SomeInterface2 getRelated();
}
public static class SomeInterfaceEntity2 implements SomeInterface2 {
// Overrides omitted for brevity
}
1 | 将 annotation 放在接口上@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");
});
接口无法定义标识符字段。 因此,它们不是存储库的有效实体类型。 |
动态或 “runtime” 托管标签
通过简单类名隐式定义或通过 Comments 显式定义的所有标签都是静态的。
它们在运行时无法更改。
如果需要可在运行时操作的其他标签,可以使用 . 是字段级别的注释,并将类型(例如 a 或 )的属性标记为动态标签的源。@Node
@DynamicLabels
@DynamicLabels
java.util.Collection<String>
List
Set
如果存在此注释,则节点上存在的所有标签以及未静态映射的 via 和类名称将在加载期间收集到该集合中。
在写入期间,节点的所有标签都将替换为静态定义的标签以及集合的内容。@Node
如果您让其他应用程序向节点添加其他标签,请不要使用 .
如果 存在于托管实体上,则生成的标签集将是写入数据库的 “事实”。@DynamicLabels @DynamicLabels |
识别实例:@Id
在类和具有特定标签的节点之间创建映射的同时,我们还需要在该类(对象)的各个实例和节点的实例之间建立连接。@Node
这就是发挥作用的地方。 将类的属性标记为对象的唯一标识符。
该唯一标识符在最佳世界中是唯一的业务密钥,或者换句话说,自然密钥。 可用于具有受支持的 simple 类型的所有属性。@Id
@Id
@Id
然而,自然键很难找到。 例如,人们的名字很少是唯一的,会随着时间的推移而变化,或者更糟糕的是,不是每个人都有名字和姓氏。
因此,我们支持两种不同类型的代理键。
在 类型的属性上,或 可以与 一起使用。 并映射到 Neo4j 内部 ID。 映射到自 Neo4j 5 以来可用的 elementId。
两者都不是节点或关系上的属性,通常对属性不可见,并允许 SDN 检索类的单个实例。String
long
Long
@Id
@GeneratedValue
Long
long
String
@GeneratedValue
提供属性 。 可用于指定实现 .
An 是一个功能接口,它采用主标签和实例来为其生成 Id 。
我们支持开箱即用的 As One Implementation。generatorClass
generatorClass
IdGenerator
IdGenerator
generateId
UUIDStringGenerator
您还可以通过 从应用程序上下文中指定 Spring Bean。
该 bean 还需要 implement ,但可以使用上下文中的所有内容,包括 Neo4j 客户端或模板来与数据库交互。@GeneratedValue
generatorRef
IdGenerator
不要跳过处理和预置唯一 ID 中有关 ID 处理的重要说明 |
乐观锁定:@Version
Spring Data Neo4j 通过在类型化字段上使用 Comments 来支持乐观锁定。
此属性将在更新期间自动递增,并且不得手动修改。@Version
Long
例如,如果不同线程中的两个事务想要使用 version 修改同一对象,则第一个操作将成功持久化到数据库。
此时,version 字段将递增,因此为 。
第二个操作将失败并显示 a ,因为它想要使用数据库中不再存在的版本修改对象。
在这种情况下,需要重试该操作,首先从数据库中重新提取具有当前版本的对象。x
x+1
OptimisticLockingFailureException
x
如果使用业务 ID,则该属性也是必需的。
Spring Data Neo4j 将检查此字段以确定该实体是新的还是之前已经持久化的。@Version
映射属性:@Property
带注释的类的所有属性都将作为 Neo4j 节点和关系的属性持久化。
无需进一步配置,Java 或 Kotlin 类中的属性名称将用作 Neo4j 属性。@Node
如果您正在使用现有的 Neo4j 模式,或者只是想根据需要调整映射,则需要使用 .
用于指定数据库中属性的名称。@Property
name
连接节点:@Relationship
该 annotation 可用于非简单类型的所有属性。
它适用于带注释的其他类型的属性或其集合及其映射。@Relationship
@Node
的 或 属性 allow configuration of the relationship 的类型允许指定方向。
SDN 中的默认方向为 。type
value
direction
Relationship.Direction#OUTGOING
我们支持动态关系。
动态关系表示为 或 。
在这种情况下,与其他域类的关系类型由 maps 键提供,并且不能通过 .Map<String, AnnotatedDomainClass>
Map<Enum, AnnotatedDomainClass>
@Relationship
地图关系属性
Neo4j 不仅支持在节点上定义属性,还支持在关系上定义属性。
为了在模型中表达这些属性,SDN 提供了应用于简单 Java 类的功能。
在 properties 类中,必须只有一个标记为 的字段来定义关系指向的实体。
或者,在关系上下文中,is coming from.@RelationshipProperties
@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 () 定义一个属性,以便 SDN 可以在保存期间确定哪些关系
可以安全地覆盖而不会丢失属性。
如果 SDN 没有找到存储内部节点 ID 的字段,则启动失败。@RelationshipId
@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 标签。
如果您只使用 plain ,则标签默认为类的名称。@Node |
2 | 每个实体都必须有一个 id。 我们使用电影的名称作为唯一标识符。 |
3 | 这显示为对字段使用与图形属性不同的名称的一种方式。@Property |
4 | 这将配置与人员的传入关系。 |
5 | 这是应用程序代码和 SDN 要使用的构造函数。 |
人员在此处映射为两个角色,和 。
domain 类是相同的: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 |