|
此版本仍在开发中,目前尚不被视为稳定版本。如需最新稳定版本,请使用 Spring Data Neo4j 8.0.4! |
基于元数据的映射
为了充分利用 SDN 中的对象映射功能,您应使用 @Node 注解来标注您的映射对象。虽然该注解并非映射框架所必需(即使不加任何注解,您的 POJO 仍能正确映射),但它可让类路径扫描器找到并预处理您的领域对象,以提取必要的元数据。如果您不使用此注解,您的应用在首次存储领域对象时会轻微影响性能,因为映射框架需要构建其内部元数据模型,以便了解您领域对象的属性及其持久化方式。
映射注解概述
来自SDN
-
@Node: 在类级别应用,以表明该类是映射到数据库的候选者。 -
@Id:应用于字段级别,用于标记用作标识目的的字段。 -
@GeneratedValue: 在字段级别与@Id一起使用,以指定如何生成唯一标识符。 -
@Property: 在字段级别应用,用于修改属性到属性的映射。 -
@CompositeProperty: 在字段级别上应用于类型为 Map 的属性,这些属性将被读回为复合类型。参见 复合属性。 -
@Relationship: 在字段级别应用,以指定关系的详细信息。 -
@DynamicLabels: 在字段级别应用,以指定动态标签的来源。 -
@RelationshipProperties: 在类级别应用,以表明该类是关系属性的目标。 -
@TargetNode: 应用于一个用@RelationshipProperties注解的类中的字段上,以从另一端的角度标记该关系的目标。
以下注解用于指定转换并确保与OGM的向后兼容性。
-
@DateLong -
@DateString -
@ConvertWith
请参阅 转换 以获取更多相关信息。
来自 Spring Data 公共模块
-
@org.springframework.data.annotation.Id与 SDN 中的@Id相同,实际上,@Id使用了 Spring Data Common 的 Id 注解进行标注。 -
@CreatedBy: 在字段级别应用,用于指示节点的创建者。 -
@CreatedDate: 在字段级别应用,以指示节点的创建日期。 -
@LastModifiedBy: 在字段级别应用,用于指示对节点最后更改的作者。 -
@LastModifiedDate: 在字段级别应用,以指示节点的最后修改日期。 -
@PersistenceCreator: Applied at one constructor to mark it as the preferred constructor when reading entities. -
@Persistent: 在类级别应用,以表明该类是映射到数据库的候选者。 -
@Version: 在字段级别应用时,用于乐观锁,并在保存操作中检查是否被修改。初始值为零,每次更新时会自动递增。 -
@ReadOnlyProperty: 在字段级别应用,用于标记一个属性为只读。该属性将在数据库读取时被填充,但不会参与写入操作。当在关系上使用时,请注意:如果未通过其他方式关联,则该集合中任何相关实体都不会被持久化。
请查看 审核 了解有关审核支持的所有注释。
基本构建块:<br/>@Node
使用“0”注释可将类标记为受映射上下文classpath扫描管理的域类。
为了将对象映射到图中的节点及其相反过程,我们需要一个标签来标识要映射的类。
@Node 有一个属性 labels ,允许您配置一个或多个标签,用于注释类的读取和写入实例时使用的。 value 属性是 labels 的别名。 如果没有指定标签,则将简单类名用作主标签。 如果要提供多个标签,可以执行以下操作:
-
提供一个数组作为
labels属性。第一个数组元素将被视为主标签。 -
为
primaryLabel提供一个值,并将额外的标签放入labels中。
标签的优先级越高,它就越具体,越接近您的领域类。要为标记指定优先级,请使用元素。
通过存储库或 Neo4j 模板编写的注释类的每个实例都会在图中写入一个节点,至少具有主标签。</p><p>反过来,所有具有主标签的节点都将映射到已注释类的实例。
关于类层次结构的注释
@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 | Which applies for the first… |
| 3 | 并且第二种实现方式同样适用 |
| 4 | (这是使用两个关系透明地为 0 指定客户端或父模型的示例。) |
| 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");
});
}
| Interfaces cannot define an identifier field. As a consequence they are not a valid entity type for repositories. |
动态或“运行时”管理的标签
所有通过简单类名隐式定义或通过 @Node 注解显式定义的标签都是静态的。 它们在运行时无法更改。 如果需要在运行时可以操作的额外标签,可以使用 @DynamicLabels。 @DynamicLabels 是字段级别的注解,它将类型为 java.util.Collection<String> 的属性(例如 List 或 Set)标记为动态标签的来源。
如果存在此注释,则在加载期间,将收集节点上存在的所有标记,而不仅仅是通过@Node和类名进行静态映射。在写入时,将使用集合中的内容以及已定义的静态标记替换节点的所有标记。
| 如果其他应用程序添加到节点,请添加其他标签,不要使用代码0。如果代码1在管理实体上存在,那么产生的标签集将是“真理”,写入数据库。 |
识别实例:@Id
而@Node会在类和具有特定标签的节点之间创建映射,我们还需要连接该类(对象)的单个实例和节点的单个实例。
这是@Id发挥作用的地方。
@Id标记类的属性,该属性是对象的唯一标识符。
在理想世界中,该唯一标识符是一个唯一的业务密钥,或者说,自然键。
@Id可以用于具有受支持的简单类型的任何属性。
自然键在实践中也很难找到。人们的名字很少是唯一的,随着时间的推移会发生变化,更糟的是,并非所有人都有第一、第二和姓氏。
因此,我们支持两种不同的替代键。
在属性类型为String、long或Long的情况下,@Id可以与@GeneratedValue一起使用。
Long和long映射到Neo4j内部id。
String映射到自Neo4j 5起可用的elementId。
这两种都不是节点或关系上的属性,通常不可见,但属性和SDN允许检索该类的单个实例。
@GeneratedValue 提供了属性generatorClass。
generatorClass 可用于指定实现IdGenerator的类。
IdGenerator是函数式接口,其generateId采用主标签和要为生成ID的实例。
我们支持UUIDStringGenerator作为开箱即用的一种实现。
您也可以通过generatorRef指定应用程序上下文中的一个Spring Bean,该@GeneratedValue。
该Bean还需要实现IdGenerator,但可以利用上下文中的一切,包括Neo4j客户端或模板来与数据库交互。
| Don't skip the important notes about ID handling in Handling and provisioning of unique IDs |
乐观锁:@Version
Spring Data Neo4j 通过在 @Version 注解的 Long 类型字段上使用 @Version 注解来支持乐观锁定。
此属性会在更新期间自动递增,不得手动修改。
如果例如两个在不同线程中的事务想要修改同一个版本为x的对象,第一个操作将成功持久化到数据库。
在这一刻,version字段将递增,因此它变为x+1。
第二个操作将因试图修改版本为x的对象而失败(此时数据库中已不存在该版本为x的对象)。
在这种情况下,该操作需要重试,从数据库中以当前版本重新获取对象后再进行处理。
The @Version 属性在使用 business ids 时也必须存在。
Spring Data Neo4j 会检查该字段以确定实体是否为新实体或在之前已持久化过。
映射属性:@Property
使用 @Node 注解的类的所有属性将作为 Neo4j 节点和关系的属性进行持久化。 在没有进一步配置的情况下,Java 或 Kotlin 类中的属性名称将用作 Neo4j 属性。
如果你正在使用现有的Neo4j模式,或者只是想根据你的需求调整映射,你将需要使用@Property。
name用于指定数据库中属性的名称。
连接节点:@Relationship
注解可以应用于所有不是简单类型的属性上。
它适用于用@Node注释的其他类型属性、集合和映射。
type 或者 value 属性允许配置关系的类型,direction 允许指定方向。
默认情况下 SDN 的方向是 Relationship.Direction#OUTGOING。
我们支持动态关系。
动态关系表示为一个 Map<String, AnnotatedDomainClass> 或 Map<Enum, AnnotatedDomainClass>。
在这种情况下,与其他领域类的关系类型由maps键给出,且不能通过 @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<>();
一个完整的例子
Putting all those together, we can create a simple domain. We use movies and people with different roles:
MovieEntityimport 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将使用的构造函数。 |
人们在这里被映射为两个角色,actors 和 directors。
域类是相同的:
PersonEntityimport 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;
}
}
| 我们还没有在两个方向上对电影和人之间的关系进行建模。 这是为什么? 我们把 0 视为聚合根,拥有这些关系。 另一方面,我们希望能够从数据库中提取所有人员而不选择与他们相关的所有电影。 请在尝试以各个方向映射数据库中的每条关系之前,考虑一下您应用程序的用例。 尽管可以这样做,但可能会在对象图中重建一个图数据库,而这并不是映射框架的意图。 如果您必须对循环或双向域进行建模,并且不想获取整个图,您可以使用查询来定义要检索的数据的细粒度说明。 |