基于元数据的映射

为了充分利用 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 的别名。如果您未指定标签,则简单类名将用作主标签。如果您想提供多个标签,您可以选择以下方式之一:

  1. labels 属性提供一个数组。数组中的第一个元素将被视为主标签。

  2. primaryLabel 提供一个值,并将附加标签放在 labels 中。

主标签应始终是反映您的域类的最具体的标签。

对于通过 repository 或 Neo4j template 写入的每个带有注解的类的实例,图数据库中将至少写入一个带有主标签的节点。反之,所有带有主标签的节点都将映射到带有注解的类的实例。

关于类层次结构的注意事项

@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");
});
接口无法定义标识符字段。因此,它们不是 repositories 的有效实体类型。

动态或“运行时”管理的标签

所有通过简单类名隐式定义或通过 @Node 注解显式定义的标签都是静态的。它们在运行时无法更改。如果您需要可以在运行时操作的附加标签,可以使用 @DynamicLabels@DynamicLabels 是字段级别的注解,将 java.util.Collection<String> 类型(例如 ListSet)的属性标记为动态标签的来源。

如果存在此注解,节点上所有未通过 @Node 和类名进行静态映射的标签将在加载期间收集到该集合中。在写入期间,节点的所有标签将被静态定义的标签加上集合的内容替换。

如果您的其他应用程序向节点添加附加标签,请不要使用 @DynamicLabels。如果托管实体上存在 @DynamicLabels,则生成的标签集将是写入数据库的“真相”。

标识实例:@Id

虽然 @Node 在类和具有特定标签的节点之间创建映射,但我们也需要在该类的单个实例(对象)和节点实例之间建立连接。

这就是 @Id 发挥作用的地方。@Id 将类的属性标记为对象的唯一标识符。在理想情况下,该唯一标识符是唯一的业务键,换句话说,是自然键。@Id 可用于所有支持简单类型的属性。

然而,自然键很难找到。例如,人们的名字很少是唯一的,会随时间变化,或者更糟的是,并非每个人都有名字和姓氏。

因此,我们支持两种不同的替代键。

StringlongLong 类型的属性上,@Id 可以与 @GeneratedValue 一起使用。Longlong 映射到 Neo4j 内部 ID。String 映射到 Neo4j 5 后可用的 elementId。两者都不是节点或关系上的属性,通常不可见。它们标识属性并允许 SDN 检索该类的单个实例。

@GeneratedValue 提供属性 generatorClassgeneratorClass 可用于指定实现 IdGenerator 的类。IdGenerator 是一个函数式接口,其 generateId 方法接受主标签和要生成 ID 的实例。我们开箱即用地支持 UUIDStringGenerator 作为一种实现。

您还可以通过 @GeneratedValuegeneratorRef 指定应用上下文中的 Spring Bean。该 bean 也需要实现 IdGenerator,但可以使用上下文中的所有内容,包括 Neo4j client 或 template 来与数据库交互。

不要跳过关于 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 或只是想根据您的需求调整映射,则需要使用 @Propertyname 属性用于指定数据库中属性的名称。

连接节点:@Relationship

@Relationship 注解可用于所有非简单类型的属性。它适用于用 @Node 注解的其他类型的属性,或它们的集合和映射。

typevalue 属性允许配置关系的类型,direction 属性允许指定方向。SDN 中的默认方向是 Relationship.Direction#OUTGOING

我们支持动态关系。动态关系表示为 Map<String, AnnotatedDomainClass>Map<Enum, AnnotatedDomainClass>。在这种情况下,与其他域类的关系类型由 Map 的键给出,并且不得通过 @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<>();

关系查询备注

一般来说,创建查询时,关系/跳数的数量没有限制。SDN 会解析从您的建模节点可达的整个图。

话虽如此,当您打算双向映射关系时,即在实体的两端都定义关系,您可能会得到比预期更多的数据。

考虑一个例子,一个 *电影* 有 *演员*,并且您想获取某个电影及其所有演员。如果从 *电影* 到 *演员* 的关系只是单向的,这不会有问题。在双向场景中,SDN 会根据关系定义获取特定的 *电影*、其 *演员*,以及为此 *演员* 定义的其他电影。在最坏的情况下,这会级联获取单个实体的整个图。

如果您必须建模循环或双向域且不想获取整个图,您可以使用projection(投影)来定义您想要获取的数据的细粒度描述。

一个完整示例

将所有这些结合起来,我们可以创建一个简单的域。我们使用具有不同角色的电影和人物
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 示例 1. MovieEntity
2 @Node 用于将此类标记为托管实体。它也用于配置 Neo4j 标签。如果您只使用简单的 @Node,标签默认采用类名。
3 每个实体都必须有一个 ID。我们使用电影名称作为唯一标识符。
4 这展示了 @Property 如何用于为字段使用与图属性不同的名称。
5 这配置了一个到人物的传入关系。

这是您的应用程序代码和 SDN 都可以使用的构造函数。

这里的人物以两种角色映射:actors(演员)和 directors(导演)。域类是相同的
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;
	}

}
示例 2. PersonEntity