|
对于最新稳定版本,请使用Spring Data Neo4j 8.0.0! |
基于元数据的映射
为了充分利用SDN中的对象映射功能,你应该用以下方式标注你映射的对象@Node注解。
虽然映射框架不一定需要这种注释(即使没有注释,你的 POJO 映射也正确),但它允许类路径扫描器找到并预处理你的域对象,提取必要的元数据。
如果你不使用这个注释,第一次存储域对象时应用会略有性能下降,因为映射框架需要构建其内部元数据模型,以便了解域对象的属性以及如何持久化它们。
映射注释概述
来自SDN
-
@Node:在类级层面应用,表示该类是映射数据库候选对象。 -
@Id:在场层级应用,用于标记用于身份目的的场域。 -
@GeneratedValue: 在现场层面应用@Id以指定唯一标识符应如何生成。 -
@Property:在字段层面应用,用于将属性映射到属性。 -
@CompositeProperty:在字段层面应用于类型为Map的属性,这些属性将被读回为复合图。参见复合性质。 -
@Relationship:在场层级应用,用于指定关系的细节。 -
@DynamicLabels:在场层级应用以指定动态标签的来源。 -
@RelationshipProperties:在类级层面应用,用以表示该类作为关系属性的目标。 -
@TargetNode: 应用于一个类域,注释为@RelationshipProperties从另一端的角度标记该关系的目标。
以下注释用于指定转换并确保与 OGM 的向后兼容。
-
@DateLong -
@DateString -
@ConvertWith
关于这方面的更多信息,请参见“转换”。
摘自春季数据共享资源
-
@org.springframework.data.annotation.Id同一个@Id事实上,来自SDN,@Id用 Spring Data Common 的 ID 注释标注。 -
@CreatedBy: 在字段层面应用以表示节点的创建者。 -
@CreatedDate: 在字段层面应用,表示节点的创建日期。 -
@LastModifiedBy:在字段层面应用,表示节点最后更改的作者。 -
@LastModifiedDate:在字段层面应用,表示节点的最后修改日期。 -
@PersistenceCreator: 在一个构造函数中应用,以标记其为读取实体时的首选构造函数。 -
@Persistent:在类级层面应用,表示该类是映射数据库候选对象。 -
@Version:在现场层面应用时,用于乐观锁定,并检查存档作的修改情况。 初始值为零,每次更新时都会自动调整。 -
@ReadOnlyProperty:在字段层面应用,用于标记属性为只读。在数据库读取过程中,属性将被水化, 但不会被写作所束缚。在关系上使用时,请注意该集合中没有相关实体会被持久化 如果没有其他关联。
可以看看Auditing,了解所有关于审计支持的注释。
基本构建模块:@Node
这@Node注释用于将类标记为受管理域类,并受映射上下文类路径扫描的约束。
要将对象映射到图中的节点,反之亦然,我们需要一个标签来标识要映射和映射的类。
@Node具有一个属性标签这允许你配置一个或多个标签,用于读写注释类实例。
这值属性是 的别名标签.
如果你没有指定标签,那么简单类名将作为主标签使用。
如果你想提供多个标签,可以选择:
-
向
标签财产。 数组中的第一个元素将被视为主标签。 -
给出一个值
主标签并加上额外的标签标签.
主标签应始终是最具体的标签,能反映你的领域类别。
对于通过仓库或 Neo4j 模板编写的注释类实例,图中至少有一个带有主标签的节点被写入。 反之,所有带有主标签的节点都会映射到注释类的实例。
关于阶级层级的说明
这@Node注释不是从超类型和接口继承而来的。
不过,你可以在每个继承层级单独注释你的域类。
这允许多态查询:你可以传递基础类或中间类,获取节点的正确具体实例。
这只支持标注为@Node.
在此类上定义的标签将作为附加标签与具体实现的标签一起使用。
我们还支持某些场景的域类层级接口:
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;
}
}
| 1 | 只是简单地用界面名称,就像你给域名命名一样 |
| 2 | 由于我们需要同步主标签,我们会将@Node在实现类上,该类
很可能是在另一个模块里。注意,该值与接口名称完全相同
实现。无法更名。 |
也可以使用不同的主标签代替接口名称:
@Node("PrimaryLabelWN") (1)
public interface SomeInterface2 {
String getName();
SomeInterface2 getRelated();
}
public static class SomeInterfaceEntity2 implements SomeInterface2 {
// 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 | 这是一个客户端或父模型,使用某种接口3显然,这两段关系都是透明的 |
| 5 | 未指定具体类型 |
所需的数据结构如下测试所示。OGM也会写同样的内容:
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>(a列表或设置例如)作为动态标签的来源。
如果有该注释,则节点上所有标签都存在且未静态映射为@Node而类别名称将在加载过程中被收集到该集合中。
写入过程中,节点的所有标签都会被静态定义的标签加上集合内容替换。
如果你有其他应用需要在节点上添加额外标签,就不要使用@DynamicLabels.
如果@DynamicLabels如果存在于托管实体中,生成的标签集就是写入数据库的“真实”标签。 |
识别实例:@Id
而@Node创建类与具有特定标签的节点之间的映射,我们还需要建立该类的各个实例(对象)与节点实例之间的连接。
这里是@Id这就开始发挥作用了。@Id将类的属性标记为对象的唯一标识符。
在理想情况下,这个唯一标识符就是唯一的业务密钥,换句话说,是自然密钥。@Id可用于所有支持简单类型的属性。
不过自然键其实很难找到。 例如,人们的名字很少是独一无二的,会随着时间变化,甚至更糟的是,并非每个人都有名字和姓氏。
因此,我们支持两种不同类型的替代密钥。
在 属性 上字符串,长或长,@Id可以用于@GeneratedValue.长和长映射到 Neo4j 内部 ID。字符串映射到自Neo4j 5以来可用的elementId。
这两个属性都不是节点或关系上的属性,通常对属性不可见,允许SDN检索该类的单个实例。
@GeneratedValue提供属性发电机类.发电机类可以用来指定一个实现的类IdGenerator.
一IdGenerator是一个函数式接口,其生成ID取主标签和实例生成 ID。
我们支持UUIDStringGenerator作为开箱即用的实现。
你也可以在应用上下文中指定一个 Spring Bean@GeneratedValue通过生成器参考.
那颗豆子也需要实现IdGenerator但可以利用上下文中的一切,包括 Neo4j 客户端或模板来与数据库交互。
| 不要跳过关于唯一ID处理与配置中关于ID处理的重要说明 |
乐观锁定:@Version
Spring Data Neo4j 支持通过使用@Version对 a 的注释长打字字段。
该属性在更新时会自动递增,且不得手动修改。
例如,如果不同线程中的两个事务想修改同一个对象的版本x,第一个作将成功持久化到数据库。
此时,版本字段会被递增,所以是x+1.
第二个作将失败,且OptimisticLockingFailureException因为它想用版本修改对象x但数据库中已不存在。
在这种情况下,作需要重新尝试,从数据库中重新获取当前版本的对象开始。
这@Version如果使用企业ID,属性也是必须的。
Spring Data Neo4j 会检查该字段,以确定该实体是新实体还是已被持久化。
属性映射:@Property
所有属性@Node-注释类将作为Neo4j节点和关系的属性被持久化。
无需进一步配置,Java 或 Kotlin 类中属性的名称将作为 Neo4j 属性使用。
如果你使用已有的 Neo4j 架构,或者只是想根据需求调整映射,你需要使用@Property.
这名称用于指定数据库中属性的名称。
连接节点:@Relationship
这@Relationship注释可用于所有非简单类型的属性。
它适用于其他类型的属性,注释为@Node或其收藏和地图。
这类型或者值属性允许配置关系类型,方向允许指定方向。
SDN的默认方向是关系。方向#外出.
我们支持动态关系。
动态关系表示为Map<String,AnnotatedDomainClass>或Map<Enum,AnnotatedDomainClass>.
在这种情况下,与其他领域类的关系类型由映射键给出,且不能通过@Relationship.
映射关系性质
Neo4j 支持不仅定义节点属性,还定义关系属性。
为了在模型中表达这些属性,SDN 提供了@RelationshipProperties应用于一个简单的 Java 类。
在属性类中必须恰好有一个字段标记为@TargetNode定义关系所指向的实体。
或者,在传入关系背景,来自。
关系属性类及其用途可能如下:
角色@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) (1)
private List<Roles> actorsAndRoles = new ArrayList<>();
一个完整的例子
将这些因素结合起来,我们可以创建一个简单的域名。 我们用电影和扮演不同角色的人:
电影实体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都应使用的构造函数。 |
这里的人被分为两个角色,演员和董事.
域类相同:
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 born;
}
public String getName() {
return name;
}
}
我们没有双向建模电影与人之间的关系。
为什么?
我们看到电影实体作为总根,拥有关系。
另一方面,我们希望能够从数据库中提取所有人,而无需选择所有与他们关联的电影。
在尝试将数据库中的每一个关系向各个方向映射之前,请考虑你的应用用例。
虽然你可以这样做,但你可能会在目标图中重建图数据库,这并不是映射框架的初衷。
如果你必须建模你的圆形或双向域,且不想取整个图,
你可以通过投影定义你想获取的数据的细粒度描述。 |