Metadata-based Mapping

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

To take full advantage of the object mapping functionality inside SDN, you should annotate your mapped objects with the @Node annotation. Although it is not necessary for the mapping framework to have this annotation (your POJOs are mapped correctly, even without any annotations), it lets the classpath scanner find and pre-process your domain objects to extract the necessary metadata. If you do not use this annotation, your application takes a slight performance hit the first time you store a domain object, because the mapping framework needs to build up its internal metadata model so that it knows about the properties of your domain object and how to persist them.

Mapping Annotation Overview

From SDN

  • @Node: Applied at the class level to indicate this class is a candidate for mapping to the database.

  • @Id: Applied at the field level to mark the field used for identity purpose.

  • @GeneratedValue: Applied at the field level together with @Id to specify how unique identifiers should be generated.

  • @Property: Applied at the field level to modify the mapping from attributes to properties.

  • @CompositeProperty: Applied at the field level on attributes of type Map that shall be read back as a composite. See Composite properties.

  • @Relationship: Applied at the field level to specify the details of a relationship.

  • @DynamicLabels: Applied at the field level to specify the source of dynamic labels.

  • @RelationshipProperties: Applied at the class level to indicate this class as the target for properties of a relationship.

  • @TargetNode: Applied on a field of a class annotated with @RelationshipProperties to mark the target of that relationship from the perspective of the other end.

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

The following annotations are used to specify conversions and ensure backwards compatibility with OGM.

  • @DateLong

  • @DateString

  • @ConvertWith

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

See Conversions for more information on that.

From Spring Data commons

  • @org.springframework.data.annotation.Id same as @Id from SDN, in fact, @Id is annotated with Spring Data Common’s Id-annotation.

  • @CreatedBy: Applied at the field level to indicate the creator of a node.

  • @CreatedDate: Applied at the field level to indicate the creation date of a node.

  • @LastModifiedBy: Applied at the field level to indicate the author of the last change to a node.

  • @LastModifiedDate: Applied at the field level to indicate the last modification date of a node.

  • @PersistenceCreator: Applied at one constructor to mark it as the preferred constructor when reading entities.

  • @Persistent: Applied at the class level to indicate this class is a candidate for mapping to the database.

  • @Version: Applied at field level it is used for optimistic locking and checked for modification on save operations. The initial value is zero which is bumped automatically on every update.

  • @ReadOnlyProperty: Applied at field level to mark a property as read only. The property will be hydrated during database reads, but not be subject to writes. When used on relationships be aware that no related entity in that collection will be persisted if not related otherwise.

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

Have a look at Auditing for all annotations regarding auditing support.

The basic building block: @Node

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

The @Node annotation is used to mark a class as a managed domain class, subject to the classpath scanning by the mapping context.

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

To map an Object to nodes in the graph and vice versa, we need a label to identify the class to map to and from.

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

@Node has an attribute labels that allows you to configure one or more labels to be used when reading and writing instances of the annotated class. The value attribute is an alias for labels. If you don’t specify a label, then the simple class name will be used as the primary label. In case you want to provide multiple labels, you could either:

  1. Supply an array to the labels property. The first element in the array will be considered as the primary label.

  2. Supply a value for primaryLabel and put the additional labels in labels.

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

The primary label should always be the most concrete label that reflects your domain class.

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

For each instance of an annotated class that is written through a repository or through the Neo4j template, one node in the graph with at least the primary label will be written. Vice versa, all nodes with the primary label will be mapped to the instances of the annotated class.

A note on class hierarchies

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

The @Node annotation is not inherited from super-types and interfaces. You can however annotate your domain classes individually at every inheritance level. This allows polymorphic queries: You can pass in base or intermediate classes and retrieve the correct, concrete instance for your nodes. This is only supported for abstract bases annotated with @Node. The labels defined on such a class will be used as additional labels together with the labels of the concrete implementations.

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

We also support interfaces in domain-class-hierarchies for some scenarios:

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 Just the plain interface name, as you would name your domain
2 As we need to synchronize the primary labels, we put @Node on the implementing class, which is probably in another module. Note that the value is exactly the same as the name of the interface implemented. Renaming is not possible.

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

Using a different primary label instead of the interface name is possible, too:

Different primary label
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$integration/shared/common/Inheritance.java[]
1 Put the @Node annotation on the interface

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

It’s also possible to use different implementations of an interface and have a polymorph domain model. When doing so, at least two labels are required: A label determining the interface and one determining the concrete class:

Multiple implementations
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$integration/shared/common/Inheritance.java[]
1 Explicitly specifying the label that identifies the interface is required in this scenario
2 Which applies for the first…
3 and second implementation as well
4 This is a client or parent model, using SomeInterface3 transparently for two relationships
5 No concrete type is specified

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

The data structure needed is shown in the following test. The same would be written by the 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[]

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

Interfaces cannot define an identifier field. As a consequence they are not a valid entity type for repositories.

Dynamic or "runtime" managed labels

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

All labels implicitly defined through the simple class name or explicitly via the @Node annotation are static. They cannot be changed during runtime. If you need additional labels that can be manipulated during runtime, you can use @DynamicLabels. @DynamicLabels is an annotation on field level and marks an attribute of type java.util.Collection<String> (a List or Set) for example) as source of dynamic labels.

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

If this annotation is present, all labels present on a node and not statically mapped via @Node and the class names, will be collected into that collection during load. During writes, all labels of the node will be replaced with the statically defined labels plus the contents of the collection.

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

If you have other applications add additional labels to nodes, don’t use @DynamicLabels. If @DynamicLabels is present on a managed entity, the resulting set of labels will be "the truth" written to the database.

Identifying instances: @Id

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

While @Node creates a mapping between a class and nodes having a specific label, we also need to make the connection between individual instances of that class (objects) and instances of the node.

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

This is where @Id comes into play. @Id marks an attribute of the class to be the unique identifier of the object. That unique identifier is in an optimal world a unique business key or in other words, a natural key. @Id can be used on all attributes with a supported simple type.

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

Natural keys are however pretty hard to find. Peoples names for example are seldom unique, change over time or worse, not everyone has a first and last name.

因此,我们支持两种不同类型的“代理键”。

We therefore support two different kind of surrogate keys.

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

On an attribute of type String, long or Long, @Id can be used with @GeneratedValue. Long and long maps to the Neo4j internal id. String maps to the elementId that is available since Neo4j 5. Both are not a property on a node or relationship and usually not visible, to the attribute and allows SDN to retrieve individual instances of the class.

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

@GeneratedValue provides the attribute generatorClass. generatorClass can be used to specify a class implementing IdGenerator. An IdGenerator is a functional interface and its generateId takes the primary label and the instance to generate an Id for. We support UUIDStringGenerator as one implementation out of the box.

可以通过 generatorRef@GeneratedValue 上指定应用程序上下文中一个 Spring Bean。该 bean 也需要实现 IdGenerator,但可以利用上下文中的一切,包括 Neo4j 客户端或模板与数据库进行交互。

You can also specify a Spring Bean from the application context on @GeneratedValue via generatorRef. That bean also needs to implement IdGenerator, but can make use of everything in the context, including the Neo4j client or template to interact with the database.

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

Don’t skip the important notes about ID handling in Handling and provisioning of unique IDs

Optimistic locking: @Version

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

Spring Data Neo4j supports optimistic locking by using the @Version annotation on a Long typed field. This attribute will get incremented automatically during updates and must not be manually modified.

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

If, e.g., two transactions in different threads want to modify the same object with version x, the first operation will get successfully persisted to the database. At this moment, the version field will get incremented, so it is x+1. The second operation will fail with a OptimisticLockingFailureException because it wants to modify the object with the version x that does not exist anymore in the database. In such cases the operation needs to get retried, beginning with a fresh fetch of the object with the current version from the database.

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

The @Version attribute is also mandatory if business ids are used. Spring Data Neo4j will check this field to determine if the entity is new or has already been persisted before.

Mapping properties: @Property

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

All attributes of a @Node-annotated class will be persisted as properties of Neo4j nodes and relationships. Without further configuration, the name of the attribute in the Java or Kotlin class will be used as Neo4j property.

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

If you are working with an existing Neo4j schema or just like to adapt the mapping to your needs, you will need to use @Property. The name is used to specify the name of the property inside the database.

Connecting nodes: @Relationship

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

The @Relationship annotation can be used on all attributes that are not a simple type. It is applicable on attributes of other types annotated with @Node or collections and maps thereof.

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

The type or the value attribute allow configuration of the relationship’s type, direction allows specifying the direction. The default direction in SDN is Relationship.Direction#OUTGOING.

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

We support dynamic relationships. Dynamic relationships are represented as a Map<String, AnnotatedDomainClass> or Map<Enum, AnnotatedDomainClass>. In such a case, the type of the relationship to the other domain class is given by the maps key and must not be configured through the @Relationship.

Map relationship properties

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

Neo4j supports defining properties not only on nodes but also on relationships. To express those properties in the model SDN provides @RelationshipProperties to be applied on a simple Java class. Within the properties class there have to be exactly one field marked as @TargetNode to define the entity the relationship points towards. Or, in an INCOMING relationship context, is coming from.

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

A relationship property class and its usage may look like this:

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 的字段,它将在启动期间失败。

You must define a property for the generated, internal ID (@RelationshipId) so that SDN can determine during save which relationships can be safely overwritten without losing properties. If SDN does not find a field for storing the internal node id, it will fail during startup.

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 从已建模的节点解析整个可到达图。

In general there is no limitation of relationships / hops for creating the queries. SDN parses the whole reachable graph from your modelled nodes.

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

This said, when there is the idea of mapping a relationship bidirectional, meaning you define the relationship on both ends of your entity, you might get more than what you are expecting.

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

Consider an example where a movie has actors, and you want to fetch a certain movie with all its actors. This won’t be problematical if the relationship from movie to actor were just unidirectional. In a bidirectional scenario SDN would fetch the particular movie, its actors but also the other movies defined for this actor per definition of the relationship. In the worst case, this will cascade to fetching the whole graph for a single entity.

A complete example

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

Putting all those together, we can create a simple domain. We use movies and people with different roles:

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 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
Unresolved include directive in modules/ROOT/pages/object-mapping/metadata-based-mapping.adoc - include::example$documentation/domain/PersonEntity.java[]

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

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.