基于元数据的映射
为了充分利用SDN(Spring Data Neo4j)中的对象映射功能,您应该使用@Node注解来标注您的映射对象。虽然映射框架不需要此注解(即使没有任何注解,您的POJO也能正确映射),但它允许类路径扫描器查找并预处理您的领域对象,以提取必要的元数据。如果您不使用此注解,那么在您第一次存储领域对象时,应用程序会受到轻微的性能影响,因为映射框架需要构建其内部元数据模型,以便了解您的领域对象的属性以及如何持久化它们。
映射注解概览
来自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注解标注的。 -
@CreatedBy:应用于字段级别,指示节点的创建者。 -
@CreatedDate:应用于字段级别,指示节点的创建日期。 -
@LastModifiedBy:应用于字段级别,指示节点最后一次更改的作者。 -
@LastModifiedDate:应用于字段级别,指示节点的最后修改日期。 -
@PersistenceCreator:应用于一个构造函数,将其标记为读取实体时的首选构造函数。 -
@Persistent:应用于类级别,表示此类是映射到数据库的候选。 -
@Version:应用于字段级别,用于乐观锁,并在保存操作时检查修改。初始值为零,每次更新时自动增加。 -
@ReadOnlyProperty:应用于字段级别,将属性标记为只读。该属性将在数据库读取期间水合,但不进行写入。当用于关系时,请注意,如果未通过其他方式关联,则该集合中的任何相关实体都不会被持久化。
有关所有审计支持的注解,请查看审计。
基本构建块:@Node
@Node注解用于将类标记为托管领域类,受映射上下文的类路径扫描。
为了将对象映射到图中的节点,反之亦然,我们需要一个标签来识别要映射的类。
@Node具有一个labels属性,允许您配置一个或多个标签,用于读取和写入注解类的实例。value属性是labels的别名。如果您未指定标签,则将使用简单的类名作为主标签。如果您想提供多个标签,可以
-
向
labels属性提供一个数组。数组中的第一个元素将被视为主标签。 -
为
primaryLabel提供一个值,并将附加标签放入labels中。
主标签应始终是最具体地反映您的领域类的标签。
对于通过存储库或Neo4j模板写入的每个注解类实例,将在图中写入至少具有主标签的一个节点。反之,所有具有主标签的节点都将映射到注解类的实例。
关于类层次结构的说明
@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 | 适用于第一个…… |
| 3 | 以及第二个实现 |
| 4 | 这是一个客户端或父模型,透明地使用SomeInterface3进行两个关系 |
| 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");
});
}
| 接口不能定义标识符字段。因此,它们不是存储库的有效实体类型。 |
动态或“运行时”管理的标签
通过简单类名隐式定义或通过@Node注解显式定义的所有标签都是静态的。它们不能在运行时更改。如果您需要可以在运行时操作的附加标签,可以使用@DynamicLabels。@DynamicLabels是字段级别的注解,将类型为java.util.Collection<String>(例如List或Set)的属性标记为动态标签的来源。
如果存在此注解,则节点上存在的所有标签(未通过@Node和类名静态映射的标签)将在加载期间收集到该集合中。在写入期间,节点的所有标签将替换为静态定义的标签加上集合的内容。
如果您有其他应用程序向节点添加附加标签,请不要使用@DynamicLabels。如果托管实体上存在@DynamicLabels,则生成的标签集将是写入数据库的“真相”。 |
标识实例:@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在@GeneratedValue上指定应用程序上下文中的Spring Bean。该Bean也需要实现IdGenerator,但可以使用上下文中的所有内容,包括Neo4j客户端或模板来与数据库交互。
| 请勿跳过唯一ID的处理和提供中关于ID处理的重要注意事项。 |
乐观锁:@Version
Spring Data Neo4j通过在Long类型字段上使用@Version注解支持乐观锁。此属性在更新期间会自动递增,不得手动修改。
例如,如果不同线程中的两个事务想要修改版本为x的同一对象,则第一个操作将成功持久化到数据库。此时,版本字段将递增,变为x+1。第二个操作将因OptimisticLockingFailureException而失败,因为它试图修改数据库中不再存在的版本x的对象。在这种情况下,操作需要重试,从数据库中重新获取具有当前版本的对象开始。
如果使用业务ID,@Version属性也是强制性的。Spring Data Neo4j将检查此字段以确定实体是新的还是之前已持久化。
映射属性:@Property
@Node注解类的所有属性都将作为Neo4j节点和关系的属性持久化。如果没有进一步配置,Java或Kotlin类中的属性名称将用作Neo4j属性。
如果您正在使用现有的Neo4j模式或只是想根据您的需求调整映射,您将需要使用@Property。name用于指定数据库内属性的名称。
连接节点:@Relationship
@Relationship注解可用于所有非简单类型的属性。它适用于用@Node注解的其他类型的属性,或其集合和映射。
type或value属性允许配置关系类型,direction允许指定方向。SDN中的默认方向是Relationship.Direction#OUTGOING。
我们支持动态关系。动态关系表示为Map<String, AnnotatedDomainClass>或Map<Enum, AnnotatedDomainClass>。在这种情况下,与其他领域类的关系类型由map的key给出,并且不得通过@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<>();
一个完整的例子
将所有这些放在一起,我们可以创建一个简单的领域模型。我们使用具有不同角色的电影和人物
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;
}
}
我们没有双向建模电影和人物之间的关系。这是为什么呢?我们将MovieEntity视为聚合根,拥有这些关系。另一方面,我们希望能够从数据库中提取所有人,而无需选择与他们相关联的所有电影。请在尝试双向映射数据库中的每个关系之前,考虑您的应用程序用例。虽然您可以这样做,但您最终可能会在对象图中重建一个图数据库,这不是映射框架的意图。如果您必须建模您的循环或双向领域模型,并且不想获取整个图,您可以通过使用投影来定义您想要获取的数据的细粒度描述。 |