此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Data Neo4j 7.4.4! |
基于元数据的映射
要充分利用 SDN 中的对象映射功能,您应该使用@Node
注解。
尽管 Map 框架不需要具有此 Comments(即使没有任何 Comments,您的 POJO 也被正确映射),但它允许 Classpath 扫描器查找并预处理您的域对象以提取必要的元数据。
如果不使用此注释,则应用程序在首次存储域对象时的性能会受到轻微影响,因为映射框架需要构建其内部元数据模型,以便它了解域对象的属性以及如何持久保存它们。
映射注释概述
从 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
与@Id
事实上,从 SDN 来看,@Id
使用 Spring Data Common 的 Id-annotation 进行注释。 -
@CreatedBy
:在字段级别应用,以指示节点的创建者。 -
@CreatedDate
:在字段级别应用,以指示节点的创建日期。 -
@LastModifiedBy
:在字段级别应用,以指示对节点的上次更改的作者。 -
@LastModifiedDate
:在字段级别应用,以指示节点的上次修改日期。 -
@PersistenceCreator
:应用于一个构造函数,以在读取实体时将其标记为首选构造函数。 -
@Persistent
:在类级别应用,以指示此类是映射到数据库的候选项。 -
@Version
:应用于字段级别,用于乐观锁定,并检查保存作的修改。 初始值为 0,每次更新时都会自动递增。 -
@ReadOnlyProperty
:在字段级别应用以将属性标记为只读。该属性将在数据库读取期间被水合, 但不受写入的约束。当用于关系时,请注意,该集合中的任何相关实体都不会被保留 如果没有其他相关。
请查看 审计 ,了解有关审计支持的所有注释。
基本构建块:@Node
这@Node
annotation 用于将类标记为托管域类,受 Mapping 上下文的 Classpath 扫描。
要将 Object 映射到图中的节点(反之亦然),我们需要一个标签来标识要映射到和从的类。
@Node
具有属性labels
这允许您配置一个或多个标签,以便在读取和写入 Annotated 类的实例时使用。
这value
attribute 是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 | 由于我们需要同步主标签,因此我们将@Node 在 implementation 类上,它
可能在另一个模块中。请注意,该值与接口的名称完全相同
实现。无法重命名。 |
也可以使用不同的主标签而不是接口名称:
@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");
});
接口无法定义标识符字段。 因此,它们不是存储库的有效实体类型。 |
动态或 “runtime” 托管标签
所有标签都通过简单的类名隐式定义,或通过@Node
注释是静态的。
它们在运行时无法更改。
如果您需要可在运行时作的其他标签,则可以使用@DynamicLabels
.@DynamicLabels
是字段级别的注释,并标记java.util.Collection<String>
(一个List
或Set
) 作为动态标签的源。
如果存在此注释,则所有标签都存在于节点上,而不是通过@Node
和类名将在加载期间收集到该集合中。
在写入期间,节点的所有标签都将替换为静态定义的标签以及集合的内容。
如果您有其他应用程序向节点添加其他标签,请不要使用@DynamicLabels .
如果@DynamicLabels 存在于托管实体上,则生成的标签集将是写入数据库的 “事实”。 |
识别实例:@Id
而@Node
在类和具有特定标签的节点之间创建映射,我们还需要在该类的各个实例(对象)和节点的实例之间建立连接。
这是@Id
开始发挥作用。@Id
将类的属性标记为对象的唯一标识符。
该唯一标识符在最佳世界中是唯一的业务密钥,或者换句话说,自然密钥。@Id
可用于具有受支持的 simple 类型的所有属性。
然而,自然键很难找到。 例如,人们的名字很少是唯一的,会随着时间的推移而变化,或者更糟糕的是,不是每个人都有名字和姓氏。
因此,我们支持两种不同类型的代理键。
在String
,long
或Long
,@Id
可与@GeneratedValue
.Long
和long
映射到 Neo4j 内部 ID。String
映射到自 Neo4j 5 以来可用的 elementId。
两者都不是节点或关系上的属性,通常对属性不可见,并允许 SDN 检索类的单个实例。
@GeneratedValue
提供属性generatorClass
.generatorClass
可用于指定实现IdGenerator
.
一IdGenerator
是一个功能接口,其generateId
获取主标签和实例以为其生成 Id 。
我们支持UUIDStringGenerator
作为一个开箱即用的实现。
您还可以从应用程序上下文中指定 Spring Bean@GeneratedValue
通过generatorRef
.
该 bean 还需要实现IdGenerator
,但可以使用上下文中的所有内容,包括 Neo4j 客户端或模板来与数据库交互。
不要跳过处理和预置唯一 ID 中有关 ID 处理的重要说明 |
乐观锁定:@Version
Spring Data Neo4j 通过使用@Version
注解Long
键入的字段。
此属性将在更新期间自动递增,并且不得手动修改。
例如,如果不同线程中的两个事务想要使用 version 修改同一个对象x
,则第一个作将成功持久化到数据库中。
此时,version 字段将递增,因此它是x+1
.
第二个作将失败,并显示OptimisticLockingFailureException
,因为它想要修改x
这在数据库中不再存在。
在这种情况下,需要重试该作,首先从数据库中重新提取具有当前版本的对象。
映射属性:@Property
的所有属性@Node
-annotated 类将持久化为 Neo4j 节点和关系的属性。
无需进一步配置,Java 或 Kotlin 类中的属性名称将用作 Neo4j 属性。
如果您正在使用现有的 Neo4j 模式,或者只是想根据您的需要调整映射,则需要使用@Property
.
这name
用于指定数据库中属性的名称。
连接节点:@Relationship
这@Relationship
annotation 可用于非简单类型的所有属性。
它适用于用@Node
或其集合和地图。
这type
或value
属性允许配置关系的类型,direction
允许指定方向。
SDN 中的默认方向为Relationship.Direction#OUTGOING
.
我们支持动态关系。
动态关系表示为Map<String, AnnotatedDomainClass>
或Map<Enum, AnnotatedDomainClass>
.
在这种情况下,与其他域类的关系类型由 maps 键提供,不得通过@Relationship
.
地图关系属性
Neo4j 不仅支持在节点上定义属性,还支持在关系上定义属性。
为了在模型中表达这些属性,SDN 提供了@RelationshipProperties
应用于简单的 Java 类。
在 properties 类中,必须只有一个字段标记为@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
is used to mark this class as a managed entity.
It also is used to configure the Neo4j label.
The label defaults to the name of the class, if you’re just using plain @Node
.
2
Each entity has to have an id.
We use the movie’s name as unique identifier.
3
This shows @Property
as a way to use a different name for the field than for the graph property.
4
This configures an incoming relationship to a person.
5
This is the constructor to be used by your application code as well as by SDN.
People are mapped in two roles here, actors
and directors
.
The domain class is the same:
Example 2. The 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;
}
}
We haven’t modelled the relationship between movies and people in both direction.
Why is that?
We see the MovieEntity
as the aggregate root, owning the relationships.
On the other hand, we want to be able to pull all people from the database without selecting all the movies associated with them.
Please consider your application’s use case before you try to map every relationship in your database in every direction.
While you can do this, you may end up rebuilding a graph database inside your object graph and this is not the intention of a mapping framework.
If you have to model your circular or bidirectional domain and don’t want to fetch the whole graph,
you can define a fine-grained description of the data that you want to fetch by using projections.