基于元数据的映射
要充分利用 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 的别名。如果您不指定标签,则使用简单类名作为主标签。如果您想提供多个标签,可以:
-
向
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;
}
}
直接使用普通接口名,正如您为领域命名那样
由于我们需要同步主标签,我们把
@Node放在实现类上(它可能在另一个模块中)。请注意,其值与所实现接口的名称完全相同。不能重命名。
也可以使用与接口名不同的主标签:
@Node("PrimaryLabelWN") 1
public interface SomeInterface2 {
String getName();
SomeInterface2 getRelated();
}
public static class SomeInterfaceEntity2 implements SomeInterface {
// Overrides omitted for brevity
}
把
@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;
}
在这种场景下,必须显式指定标识接口的标签
适用于第一个……
以及第二个实现
这是一个客户端或父模型,对两个关系透明地使用
SomeInterface3未指定具体类型
所需的数据结构如下面的测试所示:
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 schema,或只是想按自己的需求调整映射,就需要使用 @Property。name 用于指定数据库中属性的名称。
连接节点:@Relationship
@Relationship 注解可用于所有非简单类型的属性。它适用于标注了 @Node 的其他类型的属性,以及它们的集合和映射。
type 或 value 属性用于配置关系的类型,direction 用于指定方向。SDN 中的默认方向是 Relationship.Direction#OUTGOING。
我们支持动态关系。动态关系表示为 Map<String, AnnotatedDomainClass> 或 Map<Enum, AnnotatedDomainClass>。在这种情况下,到另一领域类的关系类型由 map 的键给出,不得通过 @Relationship 配置。
映射关系属性
Neo4j 不仅支持在节点上定义属性,也支持在关系上定义属性。为在模型中表达这些属性,SDN 提供了可应用于一个普通 Java 类的 @RelationshipProperties。在该属性类中,必须恰好有一个字段被标记为 @TargetNode,以定义关系指向的实体;或者在 INCOMING 关系上下文中,定义关系来自的实体。
关系属性类及其用法可能如下所示:
@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<>();
关系查询注意事项
一般来说,创建查询时对关系/跳数没有限制。SDN 会从您建模的节点解析整个可达图。
话虽如此,当您想双向映射一个关系(即在实体两端都定义该关系)时,得到的结果可能会超出您的预期。
考虑这样一个例子:一部 电影 有 演员,而您想取某部电影及其所有演员。如果从 电影 到 演员 的关系只是单向的,这不会有问题。在双向场景下,SDN 会取出该部特定 电影、其 演员,但按该关系的定义,还会取出为该 演员 定义的其他电影。在最坏情况下,这会级联到为单个实体取出整个图。
一个完整示例
把以上这些组合起来,我们可以创建一个简单的领域。我们使用电影以及扮演不同角色的人:
示例 1. 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
}
@Node用于将该类标记为受管实体。它也用于配置 Neo4j 标签。如果您只使用普通的@Node,标签默认为类名。每个实体都必须有一个 id。我们使用电影名称作为唯一标识符。
这里展示了
@Property,作为为字段使用与图属性不同名称的一种方式。这里配置了指向某个人的 incoming 关系。
这是供您应用代码以及 SDN 使用的构造函数。
这里人以两种角色映射:actors 和 directors。领域类是同一个:
示例 2. 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 this.born;
}
public String getName() {
return this.name;
}
}
我们没有在两个方向上都对电影和人之间的关系建模。为什么呢?我们把 MovieEntity 视为聚合根,拥有这些关系。另一方面,我们希望能够从数据库中取出所有人,而不必选出与他们相关的所有电影。在您试图把数据库中的每个关系都在每个方向上映射之前,请考虑您应用的用例。虽然您可以这样做,但最终可能会在您的对象图中重建一个图数据库,而这并不是映射框架的本意。如果您必须为循环或双向领域建模,又不想取出整个图,可以通过使用投影来定义您想取出数据的细粒度描述。