Metadata-based Mapping

为了充分利用 SDN 中的对象映射功能,你应该使用 @Node 注释注释你的映射对象。虽然映射框架不必具有此注释(即使没有任何注释,你的 POJO 也能正确映射),但它可以让类路径扫描器查找和预处理你的域对象,以提取必要的元数据。如果不使用此注释,则第一次存储域对象时,你的应用程序的性能会略有下降,因为映射框架需要建立其内部元数据模型,以便它了解你的域对象的属性以及如何持久保存它们。

Mapping Annotation Overview

From SDN

  • @Node:应用于类级别以指示此类是映射到数据库的候选类。

  • @Id: 在字段级别应用以标记用于标识目的的字段。

  • @GeneratedValue:与 `@Id`一起应用于字段级别以指定如何生成唯一标识符。

  • @Property:应用于字段级别以修改从属性映射到属性。

  • @CompositeProperty:应用于类型为 Map 的属性的字段级别,这些属性应读回为复合类型。请参阅 Composite properties

  • @Relationship:应用于字段级别以指定关系的详细信息。

  • @DynamicLabels:应用于字段级别以指定动态标签的来源。

  • @RelationshipProperties:应用于类级别以指示此类为关系属性的目标。

  • @TargetNode:应用于使用 `@RelationshipProperties`进行注释的类的字段,以从另一端的角度标记该关系的目标。

以下注释用于指定转换并确保与 OGM 向后兼容。

  • @DateLong

  • @DateString

  • @ConvertWith

有关该主题的更多信息,请参见 Conversions

From Spring Data commons

  • @org.springframework.data.annotation.Id`与 SDN 中的 `@Id`相同,实际上,@Id`使用 Spring Data Common 的 Id 注释进行注释。

  • @CreatedBy:应用于字段级别以指示节点的创建者。

  • @CreatedDate:应用于字段级别以指示节点的创建日期。

  • @LastModifiedBy:应用于字段级别以指示节点的最新更改的作者。

  • @LastModifiedDate:应用于字段级别以指示节点的最新修改日期。

  • @PersistenceCreator:应用于一个构造函数,以将其标记为读取实体时的首选构造函数。

  • @Persistent:应用于类级别,以指示此类是映射到数据库的候选类。

  • @Version:应用于字段级别,用于乐观锁定并在保存操作中检查修改内容。初始值为零,每次更新时都会自动增加。

  • @ReadOnlyProperty:应用于字段级别,以将属性标记为只读。该属性将在数据库读取期间填充,但不被写入。当用于关系时,请注意,该集合中无相关实体的 persaisted,除非以其他方式相关。

查看 Auditing 以了解有关审核支持的所有注释。

The basic building block: @Node

@Node 批注用于将一个类标记为受映射上下文类路径扫描约束的受管理领域类。

要在图中将 Object 映射到节点,反之亦然,我们需要一个标签来标识要映射至和映射自的类。

@Node 具有一个 labels 属性,用于配置用于在读取和写入批注类的实例时使用的多个标签。value 属性是 labels 的别名。如果您未指定标签,则将简单类名用作主标签。如果您想要提供多个标签,您可以:

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

  2. 为 `primaryLabel`提供一个值,并将其他标签放入 `labels`中。

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

对于通过存储库或 Neo4j 模板编写的批注类的每个实例,将写入至少带有主键标签的一个节点。反之,所有带有主键标签的节点都将映射到批注类的实例。

A note on class hierarchies

@Node 批注不会从超类型和接口继承。但是,您可以在每个继承级别单独注释您的领域类。这允许进行多态查询:您可以传入基础类或中间类,并为您的节点检索正确的具体实例。这仅受用 @Node 批注的抽象基础支持。此类上定义的标签将与具体实现的标签一起用作附加标签。

我们还支持一些场景中的领域类层次结构中的接口:

Domain model in a separate module, same primary label like the interface name
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$integration/shared/common/Inheritance.java[]
1 就像您命名域一样,只是简单的接口名称
2 因为我们需要同步主标签,所以我们在实现类中放置 @Node,这可能在另一个模块中。请注意,值与所实现接口的名称完全相同。不能重命名。

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

Different primary label
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$integration/shared/common/Inheritance.java[]
1 在接口中放置 `@Node`注释

还可以使用接口的不同实现,并具有一个多态的领域模型。在这样做的过程中,至少需要两个标签:一个确定接口的标签和一个确定具体类的标签:

Multiple implementations
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$integration/shared/common/Inheritance.java[]
1 在这种情况下,需要明确指定标识接口的标签
2 适用于第一个…
3 以及第二个实现
4 这是一个客户端或父模型,匿名地使用 SomeInterface3 来表示两个关系
5 未指定任何具体类型

所需的数据结构将显示在以下测试中。OGM 将编写相同的内容:

Data structure needed for using multiple, different interface implementations
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$integration/imperative/InheritanceMappingIT.java[]

接口无法定义标识符字段。因此,它们不是存储库的有效实体类型。

Dynamic or "runtime" managed labels

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

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

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

Identifying instances: @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 客户端或模板与数据库进行交互。

不要跳过 Handling and provisioning of unique IDs 中有关 ID 处理的重要说明

Optimistic locking: @Version

Spring Data Neo4j 通过在 Long 类型字段上使用 @Version 注解来支持乐观锁定。此属性会在更新期间自动加 1,并且不得手动修改。

例如,如果不同线程中的两个事务想要修改具有版本 x 的同一对象,则第一个操作将成功持久化到数据库中。此时,版本字段将加 1,变为 x+1。第二个操作将失败,显示 OptimisticLockingFailureException,因为它想要修改具有版本 x 的对象,而该版本在数据库中已不存在。在这种情况下,需要重试该操作,从通过数据库获取具有当前版本的最新对象开始。

如果使用 business ids,那么 `@Version`属性也是强制性的。Spring Data Neo4j 将检查此字段以确定实体是新建的还是已持久化过的。

Mapping properties: @Property

带有 @Node 注解的所有类属性都将持久化成 Neo4j 节点和关系的属性。如果不进行进一步配置,Java 或 Kotlin 类中属性的名称将用作 Neo4j 属性。

如果你使用现有的 Neo4j 架构或只是想将映射调整成适合你的需求,则需要使用 @Propertyname 用于指定数据库中属性的名称。

Connecting nodes: @Relationship

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

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

我们支持动态关系。动态关系表示为 Map<String, AnnotatedDomainClass>Map<Enum, AnnotatedDomainClass>。在这种情况下,与其他域类的关系类型由映射键给出,不可通过 @Relationship 进行配置。

Map relationship properties

Neo4j 支持不仅在节点上定义属性,还可以在关系上定义属性。为了在模型中表示那些属性,SDN 提供 @RelationshipProperties 应用于一个简单的 Java 类。在属性类中必须有一个字段标记为 @TargetNode,以定义关系指向的实体。或者,在 INCOMING 关系上下文中,是从中来的。

关系属性类及其用法可能如下所示:

Relationship properties Roles
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$documentation/domain/Roles.java[]

对于生成的内部 ID,你必须定义一个属性 (@RelationshipId),以便 SDN 在保存期间确定哪些关系可以在不丢失属性的情况下安全地覆盖。如果 SDN 找不到用于存储内部节点 ID 的字段,它将在启动期间失败。

Defining relationship properties for an entity
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$documentation/domain/MovieEntity.java[]

Relationship query remarks

一般来说,创建查询没有关系/跳跃限制。SDN 从已建模的节点解析整个可到达图。

话虽如此,当有将关系映射为双向的想法时,表示你在实体的两个端点上定义了该关系,你可能会得到超出你的预期的结果。

考虑一个 电影演员 的示例,并且你希望获取包含所有演员的特定电影。如果从 电影演员 的关系只是单向的,则这不会有问题。在双向场景中,SDN 将获取特定 电影、其 演员,但根据关系的定义,还将获取为该 演员 定义的其他电影。在最糟糕的情况下,这将级联到获取单个实体的整个图。

A complete example

将所有这些放在一起,我们可以创建一个简单的域。我们使用具有不同角色的电影和人物:

Example 1. The MovieEntity
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$documentation/domain/MovieEntity.java[]
1 @Node 用于将此类标记为托管实体。它还用于配置 Neo4j 标签。如果您只使用简单的 @Node ,则标签默认为类名。
2 每个实体必须有一个 ID。我们使用电影名称作为唯一标识符。
3 这展示了 @Property 作为将字段的名称用于图属性的名称不同于 graph 的方式。
4 这将配置与某人的传入关系。
5 这是由您的应用程序代码和 SDN 使用的构造器。

这里以两种角色映射人员:“演员”和“导演”。域类相同:

Example 2. The PersonEntity
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$documentation/domain/PersonEntity.java[]

我们没有在两个方向上对电影和人之间的关系进行建模。这是为什么?我们认为 MovieEntity 是拥有该关系的聚合根。另一方面,我们希望能够从数据库中提取所有人而不选择与他们关联的所有电影。在尝试以各个方向映射数据库中的每个关系之前,请考虑应用程序的用例。虽然您可以这样做,但您最终可能会在对象图中重建一个图数据库,而这并不是映射框架的意图。如果您必须对循环或双向域建模,并且不想获取整个图,您可以使用 projections 定义您想要获取的数据的细粒度描述。