参考

简介

Neo4j-OGM 是一个用于 Neo4j 的快速对象图映射库,针对使用 Cypher 的基于服务器的安装进行了优化。

它旨在简化 Neo4j 图数据库的开发,并像 JPA 一样,通过在简单的 POJO 领域对象上使用注解来实现这一目标。

Neo4j-OGM 专注于性能,引入了多项创新,包括:

  • 非基于反射的类路径扫描,可显著缩短启动时间

  • 可变深度持久化,允许您根据图的特性微调请求

  • 智能对象映射,减少对数据库的冗余请求,改善延迟并最大限度地减少 CPU 浪费

  • 用户可定义的会话生命周期,帮助您在应用程序的内存使用和服务器请求效率之间取得平衡。

概述

本参考文档分为多个章节,旨在帮助用户了解 Neo4j-OGM 如何工作的具体细节。

入门指南

入门有时可能是一项苦差事。你需要什么版本的 Neo4j-OGM?从哪里获取它们?你应该使用什么构建工具?入门 是开始的最佳位置!

配置

驱动程序、日志记录、属性、通过 Java 进行配置。如何理解所有这些选项?配置 章节涵盖了这些内容。

注解领域对象

要开始使用 Neo4j-OGM 应用程序,您只需要您的领域模型和库提供的 注解。您可以使用注解来标记领域对象,使其与图数据库中的节点和关系相对应。对于各个字段,注解允许您声明它们应如何处理并映射到图。对于属性字段和对其他实体的引用,这非常直接。由于 Neo4j 是一个无模式数据库,Neo4j-OGM 使用简单的机制通过标签将 Java 类型映射到 Neo4j 节点。实体之间的关系在图数据库中是一等公民,因此值得用 专门的一节 来描述它们在 Neo4j-OGM 中的用法。

连接到数据库

管理连接数据库的方式非常重要。连接到数据库 包含有关启动和运行所需执行操作的所有详细信息。

与图模型交互

Neo4j-OGM 提供了一个 会话 (Session) 用于与映射的实体和 Neo4j 图数据库进行交互。Neo4j 使用事务来保证数据完整性,Neo4j-OGM 完全支持这一点。事务章节描述了其中的含义。要使用 Cypher 查询等高级功能,需要对图数据模型有基本的了解。图数据模型在简介章节中进行了说明。

类型转换

Neo4j-OGM 提供对默认和定制类型转换的支持,允许您配置某些数据类型如何映射到 Neo4j 中的节点或关系。有关更多详细信息,请参阅 类型转换

过滤您的领域对象

过滤器提供了一个简单的 API,用于将条件附加到您现有的 Session.loadX() 行为中。这在 过滤器 中有更详细的介绍。

响应持久化事件

事件机制允许用户注册事件监听器,以处理与顶级对象保存以及关联对象相关的持久化事件。事件处理 讨论了使用事件的所有方面。

在您的应用程序中进行测试

有时您希望能够在内存版 Neo4j-OGM 上运行测试。测试 更详细地介绍了如何进行设置。

入门

版本

查阅版本表以确定特定版本的 Neo4j 和相关技术应使用哪个版本的 Neo4j-OGM。

兼容性

Neo4j-OGM 版本 Neo4j 版本1

4.0.x2

4.4.x6, 5.x

3.2.x

3.2.x, 3.3.x, 3.4.x, 3.5.x, 4.0.x2, 4.1.x2, 4.2.x2, 4.3.x2,5, 4.4.x2,5

3.1.x3

3.1.x, 3.2.x, 3.3.x, 3.4.x

3.0.x3

3.1.9, 3.2.12, 3.3.4, 3.4.4

2.1.x4

2.3.9, 3.0.11, 3.1.6

2.0.24

2.3.8, 3.0.7

2.0.14

2.2.x, 2.3.x

1 最新支持的错误修复版本。

2 这些版本仅支持通过 Bolt 进行连接。

3 这些版本不再积极开发。

4 这些版本不再积极开发或支持。

5 仅限 Neo4j-OGM 3.2.24+。

6 技术上可行,但未正式支持

依赖管理

为了构建应用程序,需要配置构建自动化工具以包含 Neo4j-OGM 依赖项。

Neo4j-OGM 依赖项由 neo4j-ogm-core 以及您要使用的驱动程序的相关依赖项声明组成。Neo4j-OGM 4.x 仅提供对 Bolt 驱动程序的支持,但出于兼容性原因,您必须声明依赖项

  • neo4j-ogm-bolt-driver - 使用原生 Bolt 协议在 Neo4j-OGM 和远程 Neo4j 实例之间进行通信。

  • neo4j-ogm-bolt-native-types - 通过 Bolt 协议支持 Neo4j 的所有属性类型。

Neo4j-OGM 项目可以使用 Maven、Gradle 或任何其他利用 Maven 工件仓库结构的构建系统来构建。

Maven

pom.xml<dependencies> 部分,添加以下内容

Maven 依赖项
<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-ogm-core</artifactId>
    <version>5.0.5</version>
    <scope>compile</scope>
</dependency>

<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-ogm-bolt-driver</artifactId>
    <version>5.0.5</version>
    <scope>runtime</scope>
</dependency>

另请参阅 原生类型系统,以利用 Neo4j-OGM 对原生时间和空间类型的支持。

Gradle

确保将以下依赖项添加到您的 build.gradle

Gradle 依赖项
dependencies {
    compile 'org.neo4j:neo4j-ogm-core:5.0.5'
    runtime 'org.neo4j:neo4j-ogm-bolt-driver:5.0.5'
}

配置

配置方法

有多种方法可以为 Neo4j-OGM 提供配置

  • 使用属性文件

  • 通过 Java 以编程方式使用

  • 通过提供已配置的 Neo4j Java 驱动程序实例

这些方法在下面进行了描述。它们也可以作为 示例 中的代码使用。

使用属性文件

类路径上的属性文件

ConfigurationSource props = new ClasspathConfigurationSource("my.properties");
Configuration configuration = new Configuration.Builder(props).build();

文件系统上的属性文件

ConfigurationSource props = new FileConfigurationSource("/etc/my.properties");
Configuration configuration = new Configuration.Builder(props).build();

通过 Java 以编程方式使用

在无法通过属性文件提供配置的情况下,您可以改为通过编程方式配置 Neo4j-OGM。

Configuration 对象提供了一个流畅的 API 来设置各种配置选项。然后需要将此对象提供给 SessionFactory 构造函数进行配置。

通过提供 Neo4j 驱动程序实例

只需像直接访问数据库那样配置驱动程序,然后将驱动程序实例传递给会话工厂即可。

此方法提供了最大的灵活性,并使您可以访问全方位的底层配置选项。

向 Neo4j-OGM 提供 bolt 驱动程序实例的示例
org.neo4j.driver.Driver nativeDriver = ...;
Driver ogmDriver = new BoltDriver(nativeDriver);
new SessionFactory(ogmDriver, ...);

驱动程序配置

对于通过属性文件或配置构建器进行的配置,驱动程序将自动根据给定的 URI 进行推断。空 URI 表示带有不可持续数据库的嵌入式驱动程序。

Bolt 驱动程序

请注意,对于 URI,如果没有指定端口,则使用默认的 Bolt 端口 7687。否则,可以使用 bolt://neo4j:password@localhost:1234 指定端口。

此外,bolt 驱动程序允许您定义连接池大小,这指的是每个 URL 的最大会话数。此属性是可选的,默认为 50

表 1. 基本 Bolt 驱动程序配置
ogm.properties Java 配置
URI=bolt://neo4j:password@localhost
connection.pool.size=150
Configuration configuration = new Configuration.Builder()
        .uri("bolt://neo4j:password@localhost")
        .setConnectionPoolSize(150)
        .build()

可以通过更新数据库的 neo4j.conf 来设置 Bolt 驱动程序的数据库超时。需要更改的确切设置可以在 此处找到

凭据

如果您正在使用 Bolt 驱动程序,有多种不同的方法可以为驱动程序配置提供凭据。

ogm.properties Java 配置
username="user"
password="password"
Configuration configuration = new Configuration.Builder()
             .uri("bolt://")
             .credentials("user", "password")
             .build()

注意:目前 Neo4j-OGM 仅支持基本身份验证。如果您需要使用更高级的身份验证方案,请使用原生驱动程序配置方法。

传输层安全性 (TLS/SSL)

Bolt 和 HTTP 驱动程序还允许您通过安全通道连接到 Neo4j。这些依赖于传输层安全性(即 TLS/SSL),并且需要在服务器上安装签名证书。

在某些情况下(例如某些云环境),即使您仍然希望使用加密连接,也可能无法安装签名证书。

为了支持这一点,两个驱动程序都具有配置设置,允许您绕过证书检查,尽管它们的实现方式不同。

这两种策略都会使您容易受到 MITM 攻击。除非您的服务器位于安全防火墙后,否则您可能不应该使用它们。
Bolt
ogm.properties Java 配置
#Encryption level (TLS), optional, defaults to REQUIRED.
#Valid values are NONE,REQUIRED
encryption.level=REQUIRED

#Trust strategy, optional, not used if not specified.
#Valid values are TRUST_ON_FIRST_USE,TRUST_SIGNED_CERTIFICATES
trust.strategy=TRUST_ON_FIRST_USE

#Trust certificate file, required if trust.strategy is specified
trust.certificate.file=/tmp/cert
Configuration config = new Configuration.Builder()
    ...
    .encryptionLevel("REQUIRED")
    .trustStrategy("TRUST_ON_FIRST_USE")
    .trustCertFile("/tmp/cert")
    .build();

TRUST_ON_FIRST_USE 意味着 Bolt 驱动程序将信任对主机的第一次连接是安全且有意的。在后续连接时,驱动程序将验证主机是否与第一次连接时的主机相同。

Bolt 连接测试

为了防止访问远程数据库时出现一些网络问题,您可能需要让 Bolt 驱动程序测试连接池中的连接。

当应用层和数据库之间存在防火墙时,这一点特别有用。

您可以使用连接活跃度参数来执行此操作,该参数指示测试连接的时间间隔。值为 0 表示将始终测试连接。负值表示永远不会测试连接。

ogm.properties Java 配置
# interval, in milliseconds, to check for stale db connections (test-on-borrow)
connection.liveness.check.timeout=1000
Configuration config = new Configuration.Builder()
    ...
    .connectionLivenessCheckTimeout(1000)
    .build();

主动连接验证

默认情况下,OGM 不会在应用程序启动时连接到 Neo4j 服务器。这允许您独立启动应用程序和数据库,并且将在第一次读/写时访问 Neo4j。要更改此行为,请将属性 verify.connection(或 Builder.verifyConnection(boolean))设置为 true。此设置仅对 Bolt 驱动程序有效。

日志记录

Neo4j-OGM 使用 SLF4J 来记录语句。在生产环境中,您可以在类路径根目录下创建一个名为 logback.xml 的文件中设置日志级别。请参阅 Logback 手册 以获取更多详细信息。

一个重要的记录器是 BoltResponse 记录器。它具有多个用于 Neo4j 通知类别的“子记录器”,这些通知在例如使用已弃用的功能时可能会出现。可以在下表中看到概述。

  • org.neo4j.ogm.drivers.bolt.response.BoltResponse.performance

  • org.neo4j.ogm.drivers.bolt.response.BoltResponse.hint

  • org.neo4j.ogm.drivers.bolt.response.BoltResponse.unrecognized

  • org.neo4j.ogm.drivers.bolt.response.BoltResponse.unsupported

  • org.neo4j.ogm.drivers.bolt.response.BoltResponse.deprecation

  • org.neo4j.ogm.drivers.bolt.response.BoltResponse.generic

  • org.neo4j.ogm.drivers.bolt.response.BoltResponse.security

  • org.neo4j.ogm.drivers.bolt.response.BoltResponse.topology

您仍然可以使用 org.neo4j.ogm.drivers.bolt.response.BoltResponse 记录器作为主记录器,并根据需要调整某些详细信息。

类加载优先级

在某些场景和环境中(Spring Boot 的 @Async 注解类/方法、CompletableFuture 使用等),有必要为 Neo4j-OGM 声明要使用的类加载优先级。默认情况下,它使用当前线程的上下文类加载器。要更改此行为,必须仅为 Configuration 类设置一次 OGM_CLASS_LOADER。这可以在配置您的应用程序时或类似操作中完成。

Configuration.setClassLoaderPrecedence(Configuration.ClassLoaderPrecedence.OGM_CLASS_LOADER);

注解实体

@NodeEntity: 基本构建块

@NodeEntity 注解用于声明 POJO 类是由图数据库中的节点支持的实体。由 Neo4j-OGM 处理的实体必须有一个空的公共构造函数,以允许库构造对象。

实体上的字段默认映射到节点的属性。引用其他节点实体(或其集合)的字段将通过关系链接。

@NodeEntity 注解是从超类型和接口继承的。不需要在每个继承级别注解您的领域对象。

实体字段可以使用诸如 @Property@Id@GeneratedValue@Transient@Relationship 之类的注解。所有注解都位于 org.neo4j.ogm.annotation 包中。用 transient 修饰符标记字段具有与用 @Transient 注解它相同的效果;它不会持久化到图数据库中。

持久化已注解实体
@NodeEntity
public class Actor extends DomainObject {

   @Id @GeneratedValue
   private Long id;

   @Property(name="name")
   private String fullName;

   @Property("age") // using value attribute to have a shorter definition
   private int age;

   @Relationship(type="ACTED_IN", direction=Relationship.Direction.OUTGOING)
   private List<Movie> filmography;

}

@NodeEntity(label="Film")
public class Movie {

   @Id @GeneratedValue Long id;

   @Property(name="title")
   private String name;

}

默认标签是已注解实体的简单类名。有一些规则可以确定父类是否也将其标签贡献给子类

  • 父类是非抽象类(@NodeEntity 的存在是可选的)

  • 父类是抽象类并具有 @NodeEntity 注解

  • java.lang.Object 将被忽略

  • 接口不会创建额外的标签

如果设置了 label(如上例所示)或 @NodeEntity 注解的 value 属性,它将替换应用于数据库中节点的默认标签。

使用上述注解的对象保存包含一个演员和一部电影的简单对象图,将导致以下内容被持久化到 Neo4j 中。

(:Actor:DomainObject {name:'Tom Cruise'})-[:ACTED_IN]->(:Film {title:'Mission Impossible'})

在注解对象时,您可以选择不对字段应用注解。OGM 然后将使用约定来确定每个字段在数据库中的属性名称。

持久化未注解实体
public class Actor extends DomainObject {

   private Long id;
   private String fullName;
   private List<Movie> filmography;

}

public class Movie {

   private Long id;
   private String name;

}

在这种情况下,将持久化类似于下图的图。

(:Actor:DomainObject {fullName:'Tom Cruise'})-[:FILMOGRAPHY]->(:Movie {name:'Mission Impossible'})

虽然这可以成功映射到数据库,但了解属性名称和关系类型与类的成员名称紧密耦合非常重要。重命名这些字段中的任何一个都会导致图的部分映射不正确,因此建议使用注解。

请阅读 非注解属性和最佳实践 以获取更多详细信息和最佳实践。

@Properties: 动态映射属性到图

@Properties 注解告诉 Neo4j-OGM 将节点或关系实体中 Map 字段的值映射到图中的节点或关系的属性。

属性名称派生自字段名称或 prefixdelimiter 以及 Map 中的键。例如,名称为 address 的 Map 字段包含以下条目

"street" => "Downing Street"
"number" => 10

将映射到以下节点/关系属性

address.street=Downing Street
address.number=10

Map 中键支持的类型为 StringEnum

Map 中的值可以是任何等效于 Cypher 类型的 Java 类型。如果提供了完整的类型信息,也支持其他 Java 类型。

如果注解参数 allowCast 设置为 true,则也允许转换为相应 Cypher 类型的类型。

原始类型无法推导,值将被反序列化为相应类型 - 例如,当 Integer 实例放入 Map<String, Object> 时,它将被反序列化为 Long
@NodeEntity
public class Student {

    @Properties
    private Map<String, Integer> properties = new HashMap<>();

    @Properties
    private Map<String, Object> properties = new HashMap<>();

}

运行时管理的标签

如上所述,应用于节点的标签是 @NodeEntity 标签属性的内容,如果未指定,则默认为实体的简单类名。有时可能需要在 运行时 向节点添加和删除额外的标签。我们可以使用 @Labels 注解来执行此操作。让我们提供一个设施,用于向 Student 实体添加额外标签

@NodeEntity
public class Student {

    @Labels
    private List<String> labels = new ArrayList<>();

}

现在,保存时,节点的标签将对应于实体的类层次结构 加上 后端字段包含的任何内容。我们可以在每个类层次结构中使用一个 @Labels 字段 - 它应该根据需要向子类公开或隐藏。

运行时标签不得与节点实体上定义的静态标签冲突。

在典型情况下,Neo4j-OGM 在将节点实体保存到数据库时,每个节点实体类型发出一个请求。使用许多不同的标签将导致向数据库发出许多请求(每个唯一标签组合一个请求)。

@Relationship: 连接节点实体

实体中引用一个或多个其他节点实体的每个字段都由图中的关系支持。这些关系由 Neo4j-OGM 自动管理。

最简单的关系类型是指向另一个实体的单个对象引用 (1:1)。在这种情况下,不需要对引用进行任何注解,尽管可以使用注解来控制关系的方向和类型。设置引用时,在持久化实体时会创建一个关系。如果字段设置为 null,则删除该关系。

单个关系字段
@NodeEntity
public class Movie {
    ...
    private Actor topActor;
}

也可以拥有引用实体集合 (1:N) 的字段。Neo4j-OGM 支持以下类型的实体集合

  • java.util.Vector

  • java.util.List,由 java.util.ArrayList 支持

  • java.util.SortedSet,由 java.util.TreeSet 支持

  • java.util.Set,由 java.util.HashSet 支持

  • 数组

具有关系的节点实体
@NodeEntity
public class Actor {
    ...
    @Relationship(type = "TOP_ACTOR", direction = Relationship.Direction.INCOMING)
    private Set<Movie> topActorIn;

    @Relationship("ACTS_IN") // same meaning as above but using the value attribute
    private Set<Movie> movies;
}

对于图到对象的映射,相关实体的自动传递加载取决于对 Session.load() 的调用中指定的范围深度。默认深度为 1 意味着将加载 相关 节点或关系实体并设置其属性,但不会填充其任何相关实体。

如果修改了此相关实体 Set,则一旦保存根对象(在本例中为 Actor),这些更改就会反映在图中。根据加载的根对象与保存的相应对象之间的差异,添加、删除或更新关系。

默认情况下,Neo4j-OGM 确保任意两个给定实体之间只有一种类型的关系。此规则的例外是当关系在两个相同类型的实体之间指定为 OUTGOINGINCOMING 时。在这种情况下,两个实体之间可能存在两种给定类型的关系,即每个方向上各一个关系。

如果您不关心方向,那么您可以指定 direction=Relationship.Direction.UNDIRECTED,这将确保两个节点实体之间的路径可以从任一侧导航。

例如,考虑两家公司之间的 PARTNER 关系,其中 (A)-[:PARTNER_OF]→(B) 意味着 (B)-[:PARTNER_OF]→(A)。关系的方向并不重要;重要的是这两家公司之间存在 PARTNER_OF 关系这一事实。因此,UNDIRECTED 关系是正确的选择,确保两个合作伙伴之间只有一种这种类型的关系,并且可以从任一实体导航它们。

@Relationship 上的 direction 属性默认为 OUTGOING。任何由 INCOMING 关系支持的字段或方法必须显式注解为 INCOMING 方向。

使用多于一种相同类型的关系

在某些情况下,您希望使用相同的关系类型对概念关系的两个不同方面进行建模。这里是一个规范示例

冲突的关系类型
@NodeEntity
class Person {
    private Long id;
    @Relationship(type="OWNS")
    private Car car;

    @Relationship(type="OWNS")
    private Pet pet;
...
}

这将工作得很好,但是请注意,这仅是因为结束节点类型(Car 和 Pet)是不同的类型。例如,如果您希望一个人拥有两辆车,那么您将不得不使用 Car 的 Collection 或使用不同命名的关系类型。

关系中的歧义

在关系映射可能存在歧义的情况下,建议:

  • 对象在两个方向上均可导航。

  • @Relationship 注解是显式的。

歧义关系映射的示例是多个解析为相同类型实体的关系类型,在给定方向上,但其领域对象在两个方向上不可导航。

排序

Neo4j 对关系没有任何排序,因此获取的关系没有任何特定排序。如果您想对关系集合施加顺序,有几个选项

  • 使用 SortedSet 并实现 Comparable

  • @PostLoad 注解的方法中对关系进行排序

您可以按相关节点的属性或关系属性进行排序。要按关系属性排序,您需要使用关系实体。请参阅 @RelationshipEntity: 丰富的关系

@RelationshipEntity: 丰富的关系

要访问图关系的完整数据模型,POJO 也可以用 @RelationshipEntity 注解,使它们成为关系实体。正如节点实体代表图中的节点一样,关系实体代表关系。此类 POJO 允许您访问和管理图中底层关系的属性。

关系实体中的字段类似于节点实体,因为它们被持久化为关系上的属性。为了访问关系的两个端点,可以使用两个特殊的注解:@StartNode@EndNode。用这些注解之一注解的字段将根据选择的注解提供对相应端点的访问。

为了控制关系类型,@RelationshipEntity 注解上提供了一个名为 typeString 属性。像用于标记节点实体的简单策略一样,如果没有提供此属性,则使用类的名称来导出关系类型,尽管它被转换为 SNAKE_CASE 以遵循 Neo4j 关系的命名约定。截至当前版本的 Neo4j-OGM,type 必须@RelationshipEntity 注解及其对应的 @Relationship 注解上指定。这也可以在不命名属性的情况下完成,而只提供值。

您必须在关系实体类上包含 @RelationshipEntity 以及恰好一个 @StartNode 字段和一个 @EndNode 字段,否则 Neo4j-OGM 在读取或写入时将抛出 MappingException。在未注解的领域模型中使用关系实体是不可能的。

一个简单的关系实体
@NodeEntity
public class Actor {
    Long id;
    @Relationship(type="PLAYED_IN") private Role playedIn;
}

@RelationshipEntity(type = "PLAYED_IN")
public class Role {
    @Id @GeneratedValue   private Long relationshipId;
    @Property  private String title;
    @StartNode private Actor actor;
    @EndNode   private Movie movie;
}

@NodeEntity
public class Movie {
    private Long id;
    private String title;
}

请注意,Actor 也包含对 Role 的引用。这对于持久化很重要,即使在直接保存 Role 时也是如此,因为图中的路径是先写入节点,然后在它们之间创建关系。因此,您需要构建您的领域模型,以便关系实体可以从节点实体访问,这样才能正常工作。

此外,Neo4j-OGM 不会持久化未定义任何属性的关系实体。如果您不想在关系实体中包含属性,则应该改用简单的 @Relationship。具有相同属性值且关联相同节点的多个关系实体彼此无法区分,并且由 Neo4j-OGM 表示为单个关系。

如果 @RelationshipEntity 注解是表示关系实体的类层次结构的一部分,则它必须出现在所有叶子子类上。此注解在超类上是可选的。

关于 JSON 序列化的说明

查看上面给出的示例,可以很容易地发现节点和丰富关系之间类级别的循环依赖。只要您不序列化对象,它对您的应用程序就不会有任何影响。当今使用的一种序列化是使用 Jackson 映射器的 JSON 序列化。此映射器库经常与 Spring 或 Java EE 及其对应的 Web 模块等其他框架一起使用。遍历对象树,当它在访问 Actor 后访问 Role 时,它会遇到该部分。显然,它会找到 Actor 对象并再次访问它,依此类推。这将最终导致 StackOverflowError。要打破此解析周期,强制要求通过向您的类提供注解来支持映射器。这可以通过在导致循环的属性上添加 @JsonIgnore@JsonIgnoreProperties 来完成。

抑制无限遍历
@NodeEntity
public class Actor {
    Long id;

    // Needs knowledge about the attribute "title" in the relationship.
    // Applying JsonIgnoreProperties like this ignores properties of the attribute itself.
    @JsonIgnoreProperties("actor")
    @Relationship(type="PLAYED_IN") private Role playedIn;
}

@RelationshipEntity(type="PLAYED_IN")
public class Role {
    @Id @GeneratedValue private Long relationshipId;
    @Property private String title;

    // Direct way to suppress the serialization.
    // This ignores the whole actor attribute.
    @JsonIgnore
    @StartNode private Actor actor;

    @EndNode   private Movie movie;
}

实体标识符

持久化到图中的每个节点和关系都必须有一个 ID。Neo4j-OGM 使用它来识别并将实体重新连接到内存中的图中。标识符可以是主 ID 或原生图 ID(节点创建时由 Neo4j 分配的技术 ID)。

对于主 ID,请在任何支持的类型的字段上使用 @Id,或在具有提供的 AttributeConverter 的字段上使用。为此类属性创建一个唯一索引(如果启用了索引创建)。用户代码应在创建实体实例时手动设置 ID,或者应使用 ID 生成策略。存储具有 null ID 值且没有生成策略的实体是不可能的。

在关系实体上指定主 ID 是可能的,但通过此 ID 进行查找很慢,因为 Neo4j 数据库不支持关系上的模式索引。

对于原生图 ID,请使用 @Id @GeneratedValue(默认策略为 InternalIdStrategy)。字段类型必须为 Long。此 ID 在将实体保存到图时自动分配,用户代码应 永远不要 为其赋值。

它不能是原始类型,因为那样无法表示处于瞬态的对象,因为默认值 0 将指向引用节点。

不要 在长期运行的应用程序中依赖此 ID。Neo4j 将重用已删除的节点 ID。建议用户为其领域对象想出自己的唯一标识符(或使用 UUID)。

可以通过使用 Session.load(Class<T>, ID)Session.loadAll(Class<T>, Collection<ID>) 方法按此类型的 ID 查找实体。

在一个实体中同时拥有自然 ID 和原生 ID 是可能的。在这种情况下,查找优先考虑主 ID。

如果 Long 类型的字段被简单地命名为 'id',则不需要用 @Id @GeneratedValue 注解它,因为 Neo4j-OGM 会自动将其用作原生 ID。

实体相等性

实体相等性可能是一个灰色地带。存在许多有争议的问题,例如自然键或数据库标识符是否最能描述相等性以及随时间变化的版本控制的影响。Neo4j-OGM 不强制依赖特定样式的 equals()hashCode() 实现。直接检查原生或自定义 ID 字段以查看两个实体是否表示同一个节点,并且使用 64 位哈希码进行脏检查,因此您不必以某种特定方式编写代码!

您应该以领域特定的方式为托管实体编写 equalshashCode我们强烈建议开发人员不要在这些方法中组合使用 Long 字段描述的原生 ID 和 @Id @GeneratedValue。这是因为当您第一次持久化实体时,它的哈希码会改变,因为 Neo4j-OGM 在保存时填充了数据库 ID。如果您在保存前将新创建的实体插入到基于哈希的集合中,这会导致问题。

ID 生成策略

如果单独使用 @Id 注解,则预期该字段将由应用程序代码设置。为了自动生成和分配属性的值,可以使用 @GeneratedValue 注解。

@GeneratedValue 注解具有可选参数 strategy,该参数可用于提供自定义 ID 生成策略。类必须实现 org.neo4j.ogm.id.IdStrategy 接口。策略类既可以提供无参数构造函数 - 在这种情况下,Neo4j-OGM 将创建策略实例并调用它。对于需要某些外部上下文的情况,可以通过使用 SessionFactory.register(IdStrategy) 向 SessionFactory 注册外部创建的实例。

使用 @Version 注解进行乐观锁

Neo4j-OGM 支持乐观锁以提供并发控制。要使用乐观锁,请定义一个用 @Version 注解的字段。该字段然后由 Neo4j-OGM 管理,并在更新实体时用于执行乐观锁检查。字段的类型必须为 Long,并且一个实体只能包含一个这样的字段。

使用乐观锁的典型场景如下

  • 创建新对象,版本字段包含 null

  • 保存对象时,版本字段被 Neo4j-OGM 设置为 0

  • 保存修改后的对象时,对象中提供的版本将在更新期间与数据库中的版本进行检查,如果成功,则版本在对象和数据库中都会递增

  • 如果在此期间另一个事务修改了对象(并因此递增了版本),则会检测到这一点并抛出 OptimisticLockingException

对以下内容执行乐观锁检查

  • 更新节点和关系实体的属性

  • 通过 Session.delete(T) 删除节点

  • 通过 Session.delete(T) 删除关系实体

  • 通过 Session.save(T) 检测到的删除关系实体

当发生乐观锁失败时,将在 Session 上执行以下操作

  • 乐观锁检查失败的对象将从上下文中删除,以便可以重新加载

  • 如果使用了默认事务,则将其回滚

  • 如果使用了手动事务,则它会回滚,但由于更新可能包含被主动检查的多个语句,因此未定义实际上在数据库中执行了哪些更新,建议回滚事务。如果您知道您的更新由单个修改组成,则可以选择重新加载对象并继续事务。

@Property: 属性字段的可选注解

正如我们之前提到的,不需要注解属性字段,因为它们默认被持久化。注解为 @Transient 或使用 transient 的字段被豁免于持久化。所有包含原始值的字段都直接持久化到图中。所有可以使用转换服务转换为 String 的字段都将存储为字符串。Neo4j-OGM 包括用于常用类型的默认类型转换器,完整列表请参见 内置类型转换

自定义转换器也通过使用 @Convert 指定 - 这将在 稍后 详细讨论。

原始值或可转换值的集合也会被存储。它们分别转换为其类型的数组或字符串。

节点属性名称可以通过设置 name 属性显式分配。例如 @Property(name="last_name") String lastName。如果不指定,节点属性名称默认为字段名称。

要持久化到图中的属性字段不能声明为 final

@PostLoad

一旦从数据库加载实体,就会调用用 @PostLoad 注解的方法。

非注解属性和最佳实践

Neo4j-OGM 支持映射已注解和未注解的对象模型。可以将任何没有注解的 POJO 保存到图中,因为框架应用约定来决定该怎么做。这在您无法控制要持久化的类时非常有用。然而,推荐的方法是尽可能使用注解,因为这提供了更大的控制力,并且意味着代码可以安全地重构,而不会有破坏图中标签和关系的风险。

对非注解领域类的支持在未来可能会被取消,以允许启动优化。

已注解和未注解的对象可以在同一个项目中毫无问题地使用。

每当从节点或关系构造实体时,对象图映射就会发挥作用。这可以在 Session 的查找或创建操作期间显式完成,也可以在执行任何返回节点或关系的图操作并期望返回映射实体时隐式完成。

由 Neo4j-OGM 处理的实体必须有一个空的公共构造函数,以允许库构造对象。

除非使用注解另有指定,否则框架将尝试将对象的任何“简单”字段映射到节点属性,并将任何丰富的组合对象映射到相关节点。“简单”字段是任何原始类型、装箱原始类型或 String 或其数组,本质上是任何自然适合 Neo4j 节点属性的内容。对于相关实体,关系的类型由 bean 属性名称推断。

连接到图

为了与映射的实体和 Neo4j 图进行交互,您的应用程序将需要一个 Session,它由 SessionFactory 提供。

SessionFactory

Neo4j-OGM 需要 SessionFactory 来根据需要创建 Session 实例。这也将在构造时设置对象图映射元数据,然后该元数据将在它创建的所有 Session 对象中使用。应将要扫描领域对象元数据的包提供给 SessionFactory 构造函数。

SessionFactory 是一个昂贵的创建对象,因为它扫描所有请求的包以构建元数据。它通常应该在您的应用程序生命周期中设置一次。

使用 Configuration 实例创建 SessionFactory

如配置章节所示,这是通过为 SessionFactory 提供一个配置对象来完成的

SessionFactory sessionFactory = new SessionFactory(configuration, "com.mycompany.app.domainclasses");

使用 Driver 实例创建 SessionFactory

这可以通过为 SessionFactory 提供驱动程序实例来完成

SessionFactory sessionFactory = new SessionFactory(driver, "com.mycompany.app.domainclasses");

多个实体包

也可以提供多个包。如果您宁愿只传入特定的类,也可以通过重载构造函数来做到这一点。

多个包
SessionFactory sessionFactory = new SessionFactory(configuration, "first.package.domain", "second.package.domain",...);

使用 Neo4j-OGM Session

Session 提供将对象持久化到图并以多种方式加载它们的核心功能。

会话配置

Session 用于驱动对象图映射框架。它跟踪对实体及其关系所做的更改。这样做是为了只持久化已更改的实体和关系,这在处理大型图时特别高效。一旦实体被会话跟踪,在同一会话范围内重新加载此实体将导致会话缓存返回先前加载的实体。但是,如果实体或其相关实体从图中检索到额外的关系,会话中的子图将扩展。

Session 的生命周期可以在代码中管理。例如,与单个 获取-更新-保存 循环或工作单元关联。

如果您的应用程序依赖于长期运行的会话,那么您可能看不到其他用户所做的更改,并发现自己在使用过时的对象。另一方面,如果您的会话范围太窄,则您的保存操作可能会不必要地昂贵,因为如果会话没有意识到那些最初加载的对象,更新将对所有对象进行。

因此,这两种方法之间存在权衡。通常,Session 的范围应对应于您应用程序中的“工作单元”。

如果您想从图中获取新数据,则可以通过使用新会话或使用 Session.clear() 清除当前会话上下文来实现。应谨慎使用此功能,因为它将清除整个缓存,并且需要在下一次操作时重新构建它。此外,Neo4j-OGM 将无法在由 Session.clear() 调用分隔的操作之间进行任何脏跟踪。

基本操作

基本操作仅限于对实体的 CRUD 操作和执行任意 Cypher 查询;不可能对图数据库进行更底层的操作。

鉴于 Neo4j-OGM 框架仅由 Cypher 查询驱动,因此在远程服务器模式下无法直接处理 NodeRelationship 对象。

如果您因为缺少这些功能而遇到麻烦,那么最好的选择是编写 Cypher 查询来对节点/关系执行操作。

通常,对于复杂的图遍历等低级别、超高性能操作,通过编写服务器端扩展将获得最佳性能。不过,对于大多数目的,Cypher 将足够强大且富有表现力,足以执行您需要的操作。

持久化实体

Session 允许 saveloadloadAlldelete 实体,并为您管理事务处理和异常转换。检索对象的急切程度是通过向任何加载方法指定 深度 参数来控制的。

实体持久化是通过底层 Session 对象上的 save() 方法执行的。

在底层,Session 的实现可以访问 MappingContext,它跟踪在会话期间从 Neo4j 加载的数据。在调用带有实体的 save() 时,它会检查给定的对象图与从数据库加载的数据相比是否有更改。差异用于构建一个 Cypher 查询,该查询在根据来自数据库服务器的响应重新填充其状态之前,将增量持久化到 Neo4j。

Neo4j-OGM 不会在事务关闭时自动提交,因此需要显式调用 save(…​) 才能将更改持久化到数据库。

示例 1. 持久化实体
@NodeEntity
public class Person {
   private String name;
   public Person(String name) {
      this.name = name;
   }
}

// Store Michael in the database.
Person p = new Person("Michael");
session.save(p);

保存深度

如前所述,save(entity) 被重载为 save(entity, depth),其中深度决定了从给定实体开始要保存的相关实体的数量。默认深度 -1 将持久化指定实体的属性以及从中可达到的对象图中的每个修改过的实体。这意味着从正在持久化的根对象可达到的实体模型中的 所有受影响 对象都将在图中被修改。这是推荐的方法,因为它意味着您可以在一个请求中持久化所有更改。Neo4j-OGM 能够检测哪些对象和关系需要更改,因此您不会用一堆不需要修改的对象淹没 Neo4j。您可以将持久化深度更改为任何值,但不应使其小于用于加载相应数据的值,否则您可能不会将您期望的更改实际持久化到图中。深度为 0 将仅将指定实体的属性持久化到数据库。请注意,关系操作的深度 0 总是也会影响链接的节点。

在处理复杂的集合(加载这些集合可能非常昂贵)时,指定保存深度很方便。

示例 2. 关系保存级联
@NodeEntity
class Movie {
    String title;
    Actor topActor;
    public void setTopActor(Actor actor) {
        topActor = actor;
    }
}

@NodeEntity
class Actor {
    String name;
}

Movie movie = new Movie("Polar Express");
Actor actor = new Actor("Tom Hanks");

movie.setTopActor(actor);

演员和电影都没有在图中被分配节点。如果我们调用 session.save(movie),那么 Neo4j-OGM 首先会为电影创建一个节点。然后它会注意到存在与演员的关系,因此它会以级联方式保存演员。一旦演员被持久化,它将创建从电影到演员的关系。所有这些都将在一个事务中原子地完成。

这里需要注意的重要一点是,如果改用 session.save(actor),则只会持久化演员。原因是演员实体对电影实体一无所知 - 是电影实体引用了演员。另请注意,此行为不依赖于注解上配置的任何关系方向。这是一个 Java 引用的问题,与数据库中的数据模型无关。

在以下示例中,演员和电影都是托管实体,之前都已持久化到图中

示例 3. 修改字段的级联
actor.setBirthyear(1956);
session.save(movie);

在这种情况下,即使电影有对演员的引用,对演员的属性更改 也将会 通过对 save(movie) 的调用被持久化。如上所述,原因是对于已修改且可从正在保存的根对象访问的字段,将执行级联。

在下面的示例中,session.save(user,1) 将持久化从 user 可达到的一层深度的所有修改过的对象。这包括 postsgroups,但不包括与它们相关的实体,即 authorcommentsmemberslocation。持久化深度为 0,即 session.save(user,0) 将仅保存用户的属性,忽略任何相关实体。在这种情况下,fullName 被持久化,但 friends、posts 或 groups 没有。

持久化深度
public class User  {

   private Long id;
   private String fullName;
   private List<Post> posts;
   private List<Group> groups;

}

public class Post {

   private Long id;
   private String name;
   private String content;
   private User author;
   private List<Comment> comments;

}

public class Group {

   private Long id;
   private String name;
   private List<User> members;
   private Location location;

}

加载实体

可以通过使用 session.loadXXX() 方法或通过 session.query()/session.queryForObject() 从 Neo4j-OGM 加载实体,它们将接受您自己的 Cypher 查询(请参阅下文关于 cypher 查询 的章节)。

Neo4j-OGM 包含了持久化视界(深度)的概念。在任何单个请求上,持久化视界指示在加载或保存数据时应遍历图中多少关系。视界为零意味着仅加载或保存根对象的属性,视界为 1 将包括根对象及其所有直接邻居,依此类推。此属性通过所有会话方法上可用的 depth 参数启用,但 Neo4j-OGM 会选择合理的默认值,因此您不必指定深度属性,除非您想更改默认值。

加载深度

默认情况下,加载实例将映射该对象的简单属性及其直接相关的对象(即深度 = 1)。这有助于避免意外将整个图加载到内存中,但允许单个请求不仅获取直接感兴趣的对象,还获取其最接近的邻居,这些邻居很可能也是感兴趣的。此策略试图在将太多图加载到内存和必须为数据进行重复请求之间取得平衡。

如果您的图结构的一部分是深而窄的(例如链表),您可以相应地增加这些节点的加载视界。最后,如果您的图适合内存,并且您想一次性加载它,您可以将深度设置为 -1。

另一方面,当获取可能非常“繁茂”的结构(例如本身有很多关系的事物的列表)时,您可能希望将加载视界设置为 0(深度 = 0),以避免加载您实际上不会检查的数千个对象。

当以小于会话中先前用于加载实体的自定义深度加载实体时,现有的关系不会从会话中刷新;只会添加新的实体和关系。这意味着重新加载实体将始终保留在会话中为这些实体加载的最深深度的相关对象。如果需要以比先前请求更浅的深度加载实体,则必须在新的会话中执行此操作,或者在清除当前会话 Session.clear() 后执行此操作。

加载 DTO

还可以从 Neo4j 查询任意数据,并让 OGM 将结果组合在包装对象/DTO 中。要请求 DTO,Neo4j-OGM 提供 <T> List<T> queryDto(String cypher, Map<String, ?> parameters, Class<T> type)

此 API 可能会在 Neo4j-OGM 的下一个次要/补丁版本中扩展。

查询策略

当 Neo4j-OGM 通过 load* 方法(包括带有过滤器的那些)加载实体时,它使用 LoadStrategy 来生成查询的 RETURN 部分。

可用的加载策略有

  • 模式加载策略 - 使用领域实体上的元数据和模式推导来检索节点和关系(自 Neo4j-OGM 3.0 起默认)

  • 路径加载策略 - 使用从根节点到获取相关节点的路径,p=(n)-[0..]-()(Neo4j-OGM 3.0 之前默认)

可以通过调用 SessionFactory.setLoadStrategy(strategy) 全局覆盖该策略,或者通过调用 Session.setLoadStrategy(strategy) 仅对单个会话覆盖(例如,当不同的策略对给定查询更有效时)

Cypher 查询

Cypher 是 Neo4j 强大的查询语言。Neo4j-OGM 中的所有不同驱动程序都理解它,这意味着无论您选择使用哪个驱动程序,您的应用程序代码都应该运行相同。

Session 还允许通过其 queryqueryForObject 方法执行任意 Cypher 查询。返回表格结果的 Cypher 查询应传递给 query 方法,该方法返回一个 Result。它由表示修改 Cypher 语句统计信息的 QueryStatistics(如果适用)以及包含原始数据的 Iterable<Map<String,Object>> 组成,如果需要,这些数据可以按原样使用或转换为更丰富的类型。每个 Map 中的键对应于执行的 Cypher 查询的 return 子句中列出的名称。

queryForObject 专门查询实体,因此,提供给此方法的查询必须返回节点,而不是单个属性。

在加载策略生成的查询性能不足的情况下,可以使用检索映射对象的查询方法。

此类查询应返回节点和可选的关系。为了映射关系,必须同时返回开始节点和结束节点。

返回特定领域类型的查询方法从所有结果列和这些列中的嵌套结构(例如收集的列表、映射等)中收集结果,并以单个 Iterable<T> 返回。使用 Result Session.query(java.lang.String, java.util.Map<java.lang.String,?>) 仅检索特定列中的对象。

在当前版本中,自定义查询不支持分页、排序或自定义深度。此外,它不支持将路径映射到领域实体,因此,不应从 Cypher 查询返回路径。相反,返回节点和关系以让它们映射到领域实体。

通过 Cypher 查询直接对图所做的修改将不会反映在会话中的领域对象中。

排序和分页

Neo4j-OGM 在使用 Session 对象时支持结果的排序和分页。Session 对象方法采用独立的排序和分页参数

分页
Iterable<World> worlds = session.loadAll(World.class,
                                        new Pagination(pageNumber,itemsPerPage), depth)
排序
Iterable<World> worlds = session.loadAll(World.class,
                                        new SortOrder().add("name"), depth)
降序排序
Iterable<World> worlds = session.loadAll(World.class,
                                        new SortOrder().add(SortOrder.Direction.DESC,"name"))
分页排序
Iterable<World> worlds = session.loadAll(World.class,
                                        new SortOrder().add("name"), new Pagination(pageNumber,itemsPerPage))

Neo4j-OGM 尚未支持自定义查询的排序和分页。

事务

Neo4j 是一个事务性数据库,仅允许在事务边界内执行操作。

事务可以通过在 Session 上调用 beginTransaction() 方法,然后根据需要调用 commit()rollback() 来显式管理。

事务管理
try (Transaction tx = session.beginTransaction()) {
    Person person = session.load(Person.class,personId);
    Concert concert= session.load(Concert.class,concertId);
    Hotel hotel = session.load(Hotel.class,hotelId);
    buyConcertTicket(person,concert);
    bookHotel(person, hotel);
    tx.commit();
} catch (SoldOutException e) {
    tx.rollback();
}
确保通过将事务包装在 try-with-resources 块中或通过在 finally 块中调用 close() 来始终关闭事务。

在上面的示例中,只有当音乐会门票和酒店房间都可用时,事务才会被提交,否则,两个预订都不会进行。

如果您不以这种方式管理事务,则为诸如 saveloaddeleteexecuteSession 方法隐式提供自动提交事务

事务默认是 READ_WRITE,但也可以作为 READ_ONLY 打开。

打开只读事务
Transaction tx = session.beginTransaction(Transaction.Type.READ_ONLY);
...

这对集群很重要,因为事务类型用于将请求路由到服务器。

原生属性类型

Neo4j 区分属性、结构和组合类型。虽然您可以非常轻松地将 Neo4j-OGM 实体的属性映射到组合类型,但大多数属性通常是属性类型。请阅读 使用组合类型的自定义转换器的示例 以获取有关组合类型映射的更多信息。

最重要的属性类型是

  • Number

  • 字符串

  • 布尔值

  • 空间类型 Point

  • 时间类型:DateTimeLocalTimeDateTimeLocalDateTimeDuration

Number 有两个子类型(IntegerFloat)。这些不是具有相同名称的 Java 类型,而是分别映射到 longdouble 的 Neo4j 特定类型。有关类型系统的更多信息,请参阅 Cypher 手册Java 驱动程序手册

虽然在对具有数值属性的实体进行建模时需要小心(关于精度和比例),但数字、字符串和布尔属性的映射非常直接。然而,时间和空间类型首次出现在 Neo4j 3.4 中。因此,OGM 为它们提供了 类型转换,以将它们存储为字符串或数值类型。特别地,它将时间类型映射到 ISO 8601 格式的字符串,并将空间类型映射到基于地图的组合结构。

从 Neo4j-OGM 3.2 开始,OGM 为 Neo4j 的时间和空间类型提供了专门的支持。

支持的驱动程序

Neo4j-OGM 为 Bolt 驱动程序支持所有 Neo4j 时间和空间类型。自 Neo4j-OGM 4.0 起,此支持包含在 Bolt 模块中,不需要额外的依赖项。

选择使用原生类型

使用时间和空间属性类型的原生类型是一个行为更改功能,因为它将关闭默认类型转换,并且日期不再写入字符串,也不再从字符串读取。因此,它是一个选择性功能。

要启用,请首先为您的 驱动程序 添加相应的模块,然后使用新的配置属性 use-native-types

表 2. 启用原生类型的使用
ogm.properties Java 配置
URI=bolt://neo4j:password@localhost
use-native-types=true
Configuration configuration = new Configuration.Builder()
        .uri("bolt://neo4j:password@localhost")
        .useNativeTypes()
        .build()

启用后,原生类型将用于所有节点和关系实体的所有属性,也用于通过 OGM Session 接口传递的所有参数。

原生类型的映射

下表描述了 Neo4j 时间和空间属性类型如何映射到 Neo4j-OGM 实体的属性

Neo4j 类型 Neo4j-OGM 类型

Date

LocalDate

Time

OffsetTime

LocalTime

LocalTime

DateTime

ZonedDateTime

LocalDateTime

LocalDateTime

Duration

TemporalAmount*

Point

Neo4j-OGM 空间点的一种变体**

* Neo4j Duration 可以是 Java 8 DurationPeriod,最小公分母是 TemporalAmount。Java 8 duration 总是处理精确的秒数,而 periods 在添加到即时时刻时会考虑夏令时等。如果您确定只处理其中一种,则只需使用到 java.time.Durationjava.time.Period 的显式映射。

** 没有代表空间点的通用 Java 类型。由于 OGM 支持不同的连接 Neo4j 的方式,它无法公开 Java 驱动程序或点的内部表示,因此它提供了自己的点。请阅读下一节以了解 Neo4j-OGM 为点提供了哪些具体类。

映射 Neo4j 空间类型

Neo4j 支持四种略有不同的空间点属性类型,请参阅 空间值Point 类型的所有变体都由索引支持,因此在查询中表现非常好。它们之间的主要区别是坐标参考系统。点可以存储在带有经度和纬度的地理坐标系统中,也可以存储在带有 x 和 y 的笛卡尔系统中。如果您添加第三个维度,则添加高度或 z 轴。

地理坐标系统基于球状表面,并根据角度定义球面上的位置。Neo4j 中具有地理坐标的 Point 类型属性返回 longitudelatitude,具有 WGS-84(SRID 4326,大多数 GPS 设备和许多在线地图服务器使用相同的参考系统)的固定参考系统。3 维地理坐标具有 WGS-84-3D 的参考系统,SRID 为 4979。

笛卡尔坐标系统处理欧几里得空间中的位置,且未投影。Neo4j 中具有笛卡尔坐标的 Point 类型属性返回 xy,它们的 SRID 分别为 7203 和 9157。

对您的领域建模的重要要点是,不同坐标系统的点在不进行转换的情况下是不可比较的。同一节点的属性始终应使用与具有相同标签的所有其他节点相同的坐标系统。否则,处理多个 Pointsdistance 函数和比较将返回字面量 null

为了使领域建模不易出错,Neo4j-OGM 提供了四种不同的类型,您可以在您的 Neo4j 实体中使用它们

Neo4j-OGM 类型 Neo4j 点类型

GeographicPoint2d

在 WGS-84* 的地理参考系统中具有 longitudelatitude 的点

GeographicPoint3d

在 WGS-84-3D* 的地理参考系统中具有 longitudelatitudeheight 的点

Cartesian2d

在欧几里得空间中具有 xy 的点

Cartesian3d

在欧几里得空间中具有 xyz 的点

* Neo4j 在内部排他地使用 xy(和 z),并为 longitudelatitude(和 height)提供别名。

地理点的用例是您通常在地图上找到的所有东西。笛卡尔点对于室内导航以及任何 2D 和 3D 建模非常有用。虽然地理点处理度作为单位,但笛卡尔单位本身未定义,可以是米或英尺等任何单位。

请注意,Neo4j-OGM 点没有在 Neo4j-OGM 内部之外可使用的层次结构。它应该帮助您做出明智的决定使用哪种坐标系统。

类型转换

对象图映射框架提供对默认和定制类型转换的支持,允许您配置某些数据类型如何映射到 Neo4j 中的节点或关系。如果您在 Neo4j 3.4+ 上开始新的 Neo4j 项目,则应考虑将 OGM 的 原生类型支持 用于所有时间类型。

内置类型转换

Neo4j-OGM 将自动执行以下类型转换

  • 任何扩展 java.lang.Number 的对象(包括 java.math.BigIntegerjava.math.BigDecimal)到 String 属性

  • 二进制数据(作为 byte[]Byte[])到 base-64 String,因为 Cypher 不支持字节数组

  • 使用 enum 的 name() 方法和 Enum.valueOf()java.lang.Enum 类型

  • java.util.Date 到 ISO 8601 格式的 String:"yyyy-MM-dd’T’HH:mm:ss.SSSXXX"(使用 DateString.ISO_8601

  • java.time.Instant 到带有 timezone 格式的 ISO 8601 String:"yyyy-MM-dd’T’HH:mm:ss.SSSZ"(使用 DateTimeFormatter.ISO_INSTANT

  • java.time.LocalDate 到格式为 ISO 8601 的 String:"yyyy-MM-dd"(使用 DateTimeFormatter.ISO_LOCAL_DATE

  • java.time.LocalDateTime 到格式为 ISO 8601 的 String:"yyyy-MM-dd’T’HH:mm:ss"(使用 DateTimeFormatter.ISO_LOCAL_DATE_TIME

  • java.time.OffsetDateTime 到格式为 ISO 8601 的 String:"YYYY-MM-dd’T’HH:mm:ss+01:00" / "YYYY-MM-dd’T’HH:mm:ss’Z'"(使用 DateTimeFormatter.ISO_OFFSET_DATE_TIME

基于 java.time.Instant 的日期使用 UTC 存储在数据库中。

提供了两个专用注解来修改日期转换

  1. @DateString

  2. @DateLong

它们需要应用于属性以进行自定义字符串格式化,或者如果您想将日期或日期时间值存储为 long

用户定义日期格式的示例
public class MyEntity {

    @DateString("yy-MM-dd")
    private Date entityDate;
}

或者,如果您想将 java.util.Datejava.time.Instant 存储为 long 值,请使用 @DateLong 注解

日期存储为 long 值的示例
public class MyEntity {

    @DateLong
    private Date entityDate;
}

原始值或可转换值的集合也会通过将其转换为其类型的数组或字符串来自动映射。

不支持 java.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.OffsetDateTime 的数组。不支持 java.time.Instant 的集合。

宽松转换

可以将内置转换器注解显式分配给相应的字段。这提供了能够使用将由转换器读取的 lenient 属性的优势。支持的注解是 @DateString@EnumString@NumberString。.宽松转换器使用示例

public class MyEntity {

    @DateString(lenient = true)
    private Date entityDate;
}

宽松功能目前仅受基于字符串的转换器支持,以允许转换来自数据库的空白字符串。

自定义类型转换

为了为特定成员定义定制类型转换,您可以用 @Convert 注解字段。可以使用两个转换实现之一。对于单个属性映射到单个字段的简单情况,通过类型转换,指定 AttributeConverter 的实现。

将单个属性映射到字段的示例
public class MoneyConverter implements AttributeConverter<DecimalCurrencyAmount, Integer> {

   @Override
   public Integer toGraphProperty(DecimalCurrencyAmount value) {
       return value.getFullUnits() * 100 + value.getSubUnits();
   }

   @Override
   public DecimalCurrencyAmount toEntityAttribute(Integer value) {
       return new DecimalCurrencyAmount(value / 100, value % 100);
   }

}

然后,您可以按照以下方式将其应用于您的类

@NodeEntity
public class Invoice {

   @Convert(MoneyConverter.class)
   private DecimalCurrencyAmount value;
   ...
}

当要将多个节点属性映射到单个字段时,请使用:CompositeAttributeConverter

将多个节点实体属性映射到单个类型实例的示例
/**
* This class maps latitude and longitude properties onto a Location type that encapsulates both of these attributes.
*/
public class LocationConverter implements CompositeAttributeConverter<Location> {

    @Override
    public Map<String, ?> toGraphProperties(Location location) {
        Map<String, Double> properties = new HashMap<>();
        if (location != null)  {
            properties.put("latitude", location.getLatitude());
            properties.put("longitude", location.getLongitude());
        }
        return properties;
    }

    @Override
    public Location toEntityAttribute(Map<String, ?> map) {
        Double latitude = (Double) map.get("latitude");
        Double longitude = (Double) map.get("longitude");
        if (latitude != null && longitude != null) {
            return new Location(latitude, longitude);
        }
        return null;
    }

}

就像 AttributeConverter 一样,CompositeAttributeConverter 可以按照以下方式应用于您的类

@NodeEntity
public class Person {

   @Convert(LocationConverter.class)
   private Location location;
   ...
}

过滤器

过滤器提供了一种机制,用于自定义 Neo4j-OGM 生成的 Cypher 的 where 子句。它们可以使用布尔运算符链接在一起,并与比较运算符关联。此外,每个过滤器都包含一个 FilterFunction。当实例化过滤器时,可以提供过滤器函数,否则默认使用 PropertyComparison

在下面的示例中,我们返回一个包含任何有人操纵的卫星的集合。

使用过滤器的示例
Collection<Satellite> satellites = session.loadAll(Satellite.class, new Filter("manned", ComparisonOperator.EQUALS, true));
链接过滤器的示例
Filter mannedFilter = new Filter("manned", ComparisonOperator.EQUALS, true);
Filter landedFilter = new Filter("landed", ComparisonOperator.EQUALS, false);

Filters satelliteFilter = mannedFilter.and(landedFilter);
过滤器应被视为不可变的。在以前的版本中,您可以在实例化后更改过滤器值,现在情况不再如此。

事件

Neo4j-OGM 支持持久化事件。本节描述如何拦截更新和删除事件。

您也可以查看 此处 描述的 @PostLoad 注解。

事件类型

有四种类型的事件

Event.LIFECYCLE.PRE_SAVE
Event.LIFECYCLE.POST_SAVE
Event.LIFECYCLE.PRE_DELETE
Event.LIFECYCLE.POST_DELETE

事件会为创建、更新或删除,或者以其他方式受保存或删除请求影响的每个 @NodeEntity@RelationshipEntity 对象触发。这包括

  • 正在创建、修改或删除的顶级对象或对象。

  • 任何已修改、创建或删除的关联对象。

  • 任何受图中关系创建、修改或删除影响的对象。

仅当调用 session.save()session.delete() 方法之一时,才会触发事件。使用 session.query() 直接对数据库执行 Cypher 查询不会触发任何事件。

接口

事件机制引入了两个新接口:EventEventListener

Event 接口

Event 接口由 PersistenceEvent 实现。每当应用程序希望处理事件时,它都会被赋予一个 Event 实例,该实例公开以下方法

public interface Event {

    Object getObject();
    LIFECYCLE getLifeCycle();

    enum LIFECYCLE {
        PRE_SAVE, POST_SAVE, PRE_DELETE, POST_DELETE
    }
}

事件监听器接口

EventListener 接口提供了相关方法,允许实现类处理各种不同的 Event(事件)类型。

public interface EventListener {

    void onPreSave(Event event);
    void onPostSave(Event event);
    void onPreDelete(Event event);
    void onPostDelete(Event event);

}

尽管 Event 接口允许你检索事件类型,但在大多数情况下,你的代码并不需要这样做,因为 EventListener 提供了显式捕获每种事件类型的方法。

注册事件监听器

有两种注册事件监听器的方法:

  • 在单个 Session 上注册

  • 通过使用 SessionFactory 在多个会话中注册

在本例中,我们注册了一个匿名 EventListener,以便在新对象保存之前为其注入 UUID。

class AddUuidPreSaveEventListener implements EventListener {

    void onPreSave(Event event) {
        DomainEntity entity = (DomainEntity) event.getObject():
        if (entity.getId() == null) {
            entity.setUUID(UUID.randomUUID());
        }
    }
    void onPostSave(Event event) {
    }
    void onPreDelete(Event event) {
    }
    void onPostDelete(Event event) {
}

EventListener eventListener = new AddUuidPreSaveEventListener();

// register it on an individual session
session.register(eventListener);

// remove it.
session.dispose(eventListener);

// register it across multiple sessions
sessionFactory.register(eventListener);

// remove it.
sessionFactory.deregister(eventListener);

根据应用需求,将多个 EventListener 对象添加到会话中不仅是可行的,有时也是必要的。例如,我们的业务逻辑可能要求我们为新对象添加 UUID,同时还要处理更广泛的问题,例如确保特定的持久化事件不会使我们的领域模型处于逻辑不一致的状态。通常的做法是将这些关注点分离到具有特定职责的不同对象中,而不是让单个对象试图包揽一切。

使用 EventListenerAdapter

上面的 EventListener 虽然没问题,但我们不得不为那些我们并不打算处理的事件创建三个方法。如果我们不必每次需要 EventListener 时都这样做,会更好一些。

EventListenerAdapter 是一个抽象类,它提供了 EventListener 接口的空实现(no-op)。如果你不需要处理所有类型的持久化事件,可以改为创建 EventListenerAdapter 的子类,并仅重写你感兴趣的事件类型对应的方法。

例如:

class PreSaveEventListener extends EventListenerAdapter {
    @Override
    void onPreSave(Event event) {
        DomainEntity entity = (DomainEntity) event.getObject();
        if (entity.id == null) {
            entity.UUID = UUID.randomUUID();
        }
    }
}

销毁事件监听器

需要注意的是,一旦注册了 EventListener,它将持续响应所有持久化事件。有时你可能只想在短时间内处理事件,而不是在整个会话期间都进行处理。

如果你不再需要某个 EventListener,可以通过调用 session.dispose(…​) 并传入要销毁的 EventListener,来停止其继续触发任何事件。

在将持久化事件分发给任何 EventListener 之前,收集这些事件的过程会给持久化层增加少量的性能开销。因此,如果 Session 中没有注册任何 EventListener,Neo4j-OGM 会被配置为抑制事件收集阶段。在用完事件监听器后使用 dispose() 是一个良好的实践!

要在多个会话中移除事件监听器,请使用 SessionFactory 上的 deregister 方法。

关联对象

如前所述,事件不仅针对正在保存的顶层对象触发,也会针对其所有关联对象触发。

关联对象是指从正在保存的顶层对象出发,在领域模型中可达的任何对象。关联对象在领域模型图中可以处于许多层深的位置。

通过这种方式,事件机制允许我们捕获那些并非由我们显式保存的对象的事件。

// initialise the graph
Folder folder = new Folder("folder");
Document a = new Document("a");
Document b = new Document("b");
folder.addDocuments(a, b);

session.save(folder);

// change the names of both documents and save one of them
a.setName("A");
b.setName("B");

// because `b` is reachable from `a` (via the common shared folder) they will both be persisted,
// with PRE_SAVE and POST_SAVE events being fired for each of them
session.save(a);

事件与类型

当我们删除一个类型时,图中所有带有对应于该类型标签的节点都会被删除。受影响的对象不会由事件机制枚举(它们甚至可能是未知的)。相反,系统将为该类型引发 _DELETE 事件。

    // 2 events will be fired when the type is deleted.
    // - PRE_DELETE Document.class
    // - POST_DELETE Document.class
    session.delete(Document.class);

事件与集合

保存或删除对象集合时,会为集合中的每个对象单独触发事件,而不是为集合本身触发事件。

Document a = new Document("a");
Document b = new Document("b");

// 4 events will be fired when the collection is saved.
// - PRE_SAVE a
// - PRE_SAVE b
// - POST_SAVE a
// - POST_SAVE b

session.save(Arrays.asList(a, b));

事件排序

事件是部分有序的。在同一个 savedelete 请求中,保证 PRE_ 事件在任何 POST_ 事件之前触发。然而,请求内部 PRE_ 事件和 POST_ 事件的**内部**顺序是未定义的。

示例:事件的部分排序
Document a = new Document("a");
Document b = new Document("b");

// Although the save order of objects is implied by the request, the PRE_SAVE event for `b`
// may be fired before the PRE_SAVE event for `a`, and similarly for the POST_SAVE events.
// However, all PRE_SAVE events will be fired before any POST_SAVE event.

session.save(Arrays.asList(a, b));

关系事件

前面的示例展示了当代表实体的底层**节点**在图中被更新或删除时,事件是如何触发的。当保存或删除请求导致图中**关系**的修改、添加或删除时,也会触发事件。

例如,如果你删除了包含在 Folder 的 documents 集合中的 Document 对象,那么除了 Document 之外,还会为 Folder 触发事件,以反映图中文件夹与文档之间的关系已被移除这一事实。

示例:删除附加到 FolderDocument
Folder folder = new Folder();
Document a = new Document("a");
folder.addDocuments(a);
session.save(folder);

// When we delete the document, the following events will be fired
// - PRE_DELETE a
// - POST_DELETE a
// - PRE_SAVE folder  (1)
// - POST_SAVE folder
session.delete(a);
1 请注意,folder 事件是 _SAVE 事件,而不是 _DELETE 事件。该 folder 本身并没有被删除。

事件机制不会尝试同步你的领域模型。在本例中,即使 Document 在图中已不存在,文件夹仍持有对该 Document 的引用。和往常一样,你的代码必须负责领域模型的同步。

事件唯一性

事件机制保证在保存或删除请求中,不会为同一个对象触发多次相同类型的事件。

示例:多次变更,每种类型触发一次事件
 // Even though we're making changes to both the folder node, and its relationships,
 // only one PRE_SAVE and one POST_SAVE event will be fired.
 folder.removeDocument(a);
 folder.setName("newFolder");
 session.save(folder);

测试

在测试方面有几种选择。你可以选择通过 测试工具 (Test Harness) 使用嵌入式实例,或者使用诸如 Testcontainers Neo4j 等外部库。

测试工具

使用 Neo4j-OGM 进行集成测试需要几个基本步骤:

  • org.neo4j.test:neo4j-harness 工件添加到你的 Maven / Gradle 配置中

  • 声明 Neo4jRule JUnit 规则以设置 Neo4j 测试服务器(JUnit4,运行测试工具不需要此规则)

  • 设置 Neo4j-OGM 配置和 SessionFactory

完整的运行配置示例可以在 问题模板 中找到。

日志级别

在运行单元测试时,查看 Neo4j-OGM 的执行过程非常有用,特别是查看在你的应用程序和数据库之间传输的 Cypher 请求。Neo4j-OGM 使用 slf4jLogback 作为其日志框架。默认情况下,所有 Neo4j-OGM 组件的日志级别设置为 WARN,这不包含任何 Cypher 输出。要更改 Neo4j-OGM 日志级别,请在测试资源文件夹中创建一个 logback-test.xml 文件,并按如下所示进行配置:

logback-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d %5p %40.40c:%4L - %m%n</pattern>
        </encoder>
    </appender>

    <!--
      ~ Set the required log level for Neo4j-OGM components here.
      ~ To just see Cypher statements set the level to "info"
      ~ For finer-grained diagnostics, set the level to "debug".
    -->
    <logger name="org.neo4j.ogm" level="info" />

    <root level="warn">
        <appender-ref ref="console" />
    </root>

</configuration>