基于元数据的映射

为了充分利用SDN(Spring Data Neo4j)中的对象映射功能,您应该使用@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中。

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

对于通过存储库或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;
    }
}
1 使用纯接口名称,就像您命名您的领域一样
2 由于我们需要同步主标签,我们将@Node放在实现类上,该类可能在另一个模块中。请注意,值与所实现接口的名称完全相同。不允许重命名。

使用不同的主标签而不是接口名称也是可能的

不同的主标签
@Node("PrimaryLabelWN") (1)
public interface SomeInterface2 {

    String getName();

    SomeInterface2 getRelated();
}

public static class SomeInterfaceEntity2 implements SomeInterface {

    // 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 未指定具体类型

所需的数据结构显示在以下测试中

使用多个不同接口实现所需的数据结构
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>(例如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作为一种实现。

您还可以通过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模式或只是想根据您的需求调整映射,您将需要使用@Propertyname用于指定数据库内属性的名称。

连接节点:@Relationship

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

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

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

}
1 @Node用于将此类标记为托管实体。它也用于配置Neo4j标签。如果您只使用纯@Node,则标签默认为类的名称。
2 每个实体都必须有一个ID。我们使用电影的名称作为唯一标识符。
3 这显示了@Property,它是一种为字段使用与图属性不同名称的方法。
4 这配置了一个指向人物的传入关系。
5 这是您的应用程序代码和SDN都将使用的构造函数。

人物在这里以两种角色映射,actorsdirectors。领域类是相同的

示例 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视为聚合根,拥有这些关系。另一方面,我们希望能够从数据库中提取所有人,而无需选择与他们相关联的所有电影。请在尝试双向映射数据库中的每个关系之前,考虑您的应用程序用例。虽然您可以这样做,但您最终可能会在对象图中重建一个图数据库,这不是映射框架的意图。如果您必须建模您的循环或双向领域模型,并且不想获取整个图,您可以通过使用投影来定义您想要获取的数据的细粒度描述。
© . This site is unofficial and not affiliated with VMware.