原标题:Spring认证中国教育管理中心-Spring Data Neo4j教程二(Spring中国教育管理中心)
6. 对象映射
以下部分将解释图表和域之间的映射过程。它分为两部分。第一部分解释了实际映射和可用工具,用于描述如何将节点、关系和属性映射到对象。第二部分将介绍 Spring Data 的对象映射基础知识。它提供了有关通用映射的宝贵提示,为什么您应该更喜欢不可变域对象以及如何使用 Java 或 Kotlin 对它们进行建模。
6.1。基于元数据的映射
要充分利用 SDN 中的对象映射功能,您应该使用注解对映射的对象进行@Node注解。尽管映射框架没有必要具有此注释(您的 POJO 已正确映射,即使没有任何注释),但它允许类路径扫描器查找并预处理您的域对象以提取必要的元数据。如果你不使用这个注解,你的应用程序在你第一次存储一个域对象时会受到轻微的性能影响,因为映射框架需要建立它的内部元数据模型,以便它知道你的域对象的属性以及如何坚持他们。
6.1.1.映射注释概述
来自 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-annotation 标注的。
- @CreatedBy:应用于字段级别以指示节点的创建者。
- @CreatedDate:应用于字段级别以指示节点的创建日期。
- @LastModifiedBy:应用于字段级别以指示对节点的最后更改的作者。
- @LastModifiedDate:在字段级别应用以指示节点的最后修改日期。
- @PersistenceConstructor:应用于一个构造函数,以在读取实体时将其标记为首选构造函数。
- @Persistent:在类级别应用以指示该类是映射到数据库的候选对象。
- @Version:应用于字段级别,用于乐观锁定并检查保存操作的修改。初始值为零,每次更新时都会自动增加。
- @ReadOnlyProperty:应用于字段级别以将属性标记为只读。该属性将在数据库读取期间被水合,但不受写入影响。当用于关系时,请注意,如果不相关,则该集合中的任何相关实体都不会保留。
查看第 10 章,了解有关审计支持的所有注释。
6.1.2.基本构建块:@Node
注解用于将@Node类标记为受管域类,受映射上下文的类路径扫描。
要将对象映射到图中的节点,反之亦然,我们需要一个标签来标识要映射到和从的类。
@Node有一个属性labels,允许您配置一个或多个标签,以便在读取和写入带注释的类的实例时使用。该value属性是 的别名labels。如果您不指定标签,则简单类名将用作主标签。如果您想提供多个标签,您可以:
- 为属性提供一个数组labels。数组中的第一个元素将被视为主标签。
- 为 提供一个值primaryLabel并将附加标签放入labels.
主标签应始终是反映您的域类的最具体的标签。
对于通过存储库或通过 Neo4j 模板编写的注释类的每个实例,将写入图中至少具有主标签的一个节点。反之亦然,所有具有主标签的节点都将映射到注释类的实例。
关于类层次结构的说明
@Node注解不是从超类型和接口继承的。但是,您可以在每个继承级别单独注释您的域类。这允许多态查询:您可以传入基类或中间类并为您的节点检索正确的具体实例。这仅支持带有注释的抽象基@Node。在此类上定义的标签将与具体实现的标签一起用作附加标签。
对于某些场景,我们还支持域类层次结构中的接口:
清单 10. 单独模块中的域模型,与接口名称相同的主标签
代码语言:javascript复制public interface SomeInterface {
String getName();
SomeInterface getRelated();
}
@Node("SomeInterface")
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;
}
}
只是简单的接口名称,就像您命名您的域一样
由于我们需要同步主标签,我们放置@Node了实现类,它可能在另一个模块中。请注意,该值与实现的接口名称完全相同。重命名是不可能的。
也可以使用不同的主标签而不是接口名称:
清单 11. 不同的主标签
代码语言:javascript复制@Node("PrimaryLabelWN")
public interface SomeInterface2 {
String getName();
SomeInterface2 getRelated();
}
public static class SomeInterfaceEntity2 implements SomeInterface2 {
// Overrides omitted for brevity
}
将@Node注解放在界面上
还可以使用接口的不同实现并具有多态域模型。这样做时,至少需要两个标签:一个确定接口的标签和一个确定具体类的标签:
清单 12. 多个实现
代码语言:javascript复制@Node("SomeInterface3")
public interface SomeInterface3 {
String getName();
SomeInterface3 getRelated();
}
@Node("SomeInterface3a")
public static class SomeInterfaceImpl3a implements SomeInterface3 {
// Overrides omitted for brevity
}
@Node("SomeInterface3b")
public static class SomeInterfaceImpl3b implements SomeInterface3 {
// Overrides omitted for brevity
}
@Node
public static class ParentModel {
@Id
@GeneratedValue
private Long id;
private SomeInterface3 related1;
private SomeInterface3 related2;
}
本场景需要显式指定标识接口的标签
这适用于第一个……
以及第二次实施
这是一个客户端或父模型,SomeInterface3透明地用于两个关系
未指定具体类型
所需的数据结构如下面的测试所示。OGM 也会这样写:
清单 13. 使用多个不同接口实现所需的数据结构
代码语言:javascript复制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");
});
接口不能定义标识符字段。因此,它们不是存储库的有效实体类型。
动态或“运行时”托管标签
通过简单类名隐式定义或通过@Node注释显式定义的所有标签都是静态的。它们不能在运行时更改。如果您需要可以在运行时操作的其他标签,您可以使用@DynamicLabels. @DynamicLabels是字段级别的注释,并将类型java.util.Collection<String>(例如 aList或Set)的属性标记为动态标签的来源。
如果存在此注释,则节点上存在且未通过静态映射的所有标签@Node和类名称将在加载期间收集到该集合中。在写入期间,节点的所有标签都将替换为静态定义的标签加上集合的内容。
如果您有其他应用程序向节点添加其他标签,请不要使用@DynamicLabels. 如果@DynamicLabels存在于托管实体上,则生成的标签集将是写入数据库的“真相”。
6.1.3.识别实例:@Id
在@Node创建类和具有特定标签的节点之间的映射时,我们还需要在该类(对象)的各个实例和节点实例之间建立连接。
这就是@Id发挥作用的地方。 @Id将类的属性标记为对象的唯一标识符。该唯一标识符在最佳世界中是唯一的业务密钥,或者换句话说,是自然密钥。 @Id可用于所有受支持的简单类型的属性。
然而,自然键很难找到。例如,人们的名字很少是唯一的,随着时间的推移而变化或更糟,不是每个人都有名字和姓氏。
因此,我们支持两种不同类型的代理键。
long在或类型的属性上Long,@Id可以与 一起使用@GeneratedValue。这会将 Neo4j 内部 id(不是节点或关系上的属性,通常不可见)映射到属性,并允许 SDN 检索类的各个实例。
@GeneratedValue提供属性generatorClass。 generatorClass可用于指定实现IdGenerator. AnIdGenerator是一个功能接口,它generateId采用主标签和实例来为其生成 Id。我们支持UUIDStringGenerator作为一种开箱即用的实现。
您还可以在@GeneratedValuevia上从应用程序上下文中指定一个 Spring Bean generatorRef。该 bean 也需要实现IdGenerator,但可以利用上下文中的所有内容,包括与数据库交互的 Neo4j 客户端或模板。
6.1.4。乐观锁定:@Version
Spring Data Neo4j 通过在类型化字段上使用@Version注释来支持乐观锁定。Long此属性将在更新期间自动递增,不得手动修改。
例如,如果不同线程中的两个事务想要使用 version 修改同一个对象x,则第一个操作将成功持久化到数据库中。此时版本字段会递增,所以是x 1. 第二个操作将失败, OptimisticLockingFailureException因为它想用x 数据库中不再存在的版本修改对象。在这种情况下,操作需要重试,从从数据库中重新获取具有当前版本的对象开始。
6.1.5。映射属性:@Property
-annotated 类的所有属性@Node都将作为 Neo4j 节点和关系的属性持久化。无需进一步配置,Java 或 Kotlin 类中的属性名称将用作 Neo4j 属性。
如果您正在使用现有的 Neo4j 架构,或者只是想根据您的需要调整映射,则需要使用@Property. name用于指定数据库内属性的名称。
6.1.6。连接节点:@Relationship
@Relationship注解可用于所有非简单类型的属性。它适用于用其他类型注释的属性@Node或其集合和映射。
type或value属性允许配置关系的类型,允许direction指定方向。SDN 中的默认方向是Relationship.Direction#OUTGOING.
我们支持动态关系。动态关系表示为Map<String, AnnotatedDomainClass>或Map<Enum, AnnotatedDomainClass>。在这种情况下,与其他域类的关系类型由 maps 键给出,不能通过@Relationship.
映射关系属性
Neo4j 不仅支持在节点上定义属性,还支持在关系上定义属性。为了在模型中表达这些属性,SDN 提供@RelationshipProperties了应用于一个简单的 Java 类。在属性类中,必须恰好有一个字段被标记为@TargetNode定义关系指向的实体。或者,在INCOMING关系上下文中,来自。
关系属性类及其用法可能如下所示:
清单 14. 关系属性 Roles
代码语言:javascript复制@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;
}
}
您必须为生成的内部 ID ( @RelationshipId) 定义一个属性,以便 SDN 可以在保存期间确定可以安全覆盖哪些关系而不会丢失属性。如果 SDN 没有找到存储内部节点 id 的字段,它会在启动过程中失败。
清单 15. 为实体定义关系属性
代码语言:javascript复制@Relationship(type = "ACTED_IN", direction = Direction.INCOMING)
private List<Roles> actorsAndRoles;
关系查询备注
一般来说,创建查询的关系/跃点没有限制。SDN 从您的建模节点解析整个可达图。
这就是说,当存在双向映射关系的想法时,这意味着您在实体的两端定义关系,您可能会得到比您期望的更多的东西。
考虑一个电影有演员的例子,你想获取某部电影及其所有演员。如果从电影到演员的关系只是单向的,这不会有问题。在双向场景中,SDN 将获取特定电影、其演员以及根据关系定义为该演员定义的其他电影。在最坏的情况下,这将级联到获取单个实体的整个图。
6.1.7。一个完整的例子
将所有这些放在一起,我们可以创建一个简单的域。我们使用不同角色的电影和人物:
示例 3. MovieEntity
代码语言:javascript复制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")
public class MovieEntity {
@Id
private final String title;
@Property("tagline")
private final String description;
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING)
private List<Roles> actorsAndRoles;
@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();
public MovieEntity(String title, String description) {
this.title = title;
this.description = description;
}
// Getters omitted for brevity
}
@Node用于将此类标记为托管实体。它还用于配置 Neo4j 标签。如果您只是使用 plain ,标签默认为类的名称@Node。
每个实体都必须有一个 id。我们使用电影的名称作为唯一标识符。
这显示@Property了为字段使用与图形属性不同的名称的一种方式。
这配置了与人的传入关系。
这是您的应用程序代码和 SDN 使用的构造函数。
人们在这里被映射为两个角色,actors并且directors。域类是一样的:
示例 4. PersonEntity
代码语言:javascript复制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;
}
}
我们还没有在两个方向上模拟电影和人之间的关系。这是为什么?我们将MovieEntity视为聚合根,拥有关系。另一方面,我们希望能够从数据库中提取所有人,而无需选择与他们关联的所有电影。在尝试将数据库中的每个关系映射到各个方向之前,请考虑您的应用程序的用例。虽然您可以这样做,但您最终可能会在对象图中重建图形数据库,这不是映射框架的意图。