参考

简介

Neo4j-OGM 是一个快速的对象图映射库,专为利用 Cypher 的服务器端安装而优化。

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

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

  • 基于非反射的类路径扫描,启动时间更快

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

  • 智能对象映射,减少对数据库的冗余请求,提高延迟并最大程度地减少浪费的 CPU 周期

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

概述

本参考文档分为多个部分,以帮助用户理解 Neo4j-OGM 的具体工作方式。

开始使用

开始使用有时会很麻烦。您需要哪个版本的 Neo4j-OGM?从哪里获取它们?应该使用哪个构建工具?开始使用正是开始的理想之地!

配置

驱动程序、日志记录、属性、通过 Java 配置。如何理解所有选项?配置部分将为您解答。

注解您的领域对象

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

连接到数据库

管理您如何连接到数据库非常重要。连接到数据库部分包含您开始运行所需的所有详细信息。

与图模型交互

Neo4j-OGM 提供会话,用于与映射的实体和 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>4.0.18</version>
    <scope>compile</scope>
</dependency>

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

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

Gradle

确保以下依赖项已添加到您的 build.gradle 文件中

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

配置

配置方法

有几种方法可以为 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()

可以使用 Bolt 驱动程序设置数据库超时,方法是更新您的数据库的 neo4j.conf。要更改的确切设置可在此处找到

凭据

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

ogm.properties Java 配置
username="user"
password="password"
Configuration configuration = new Configuration.Builder()
             .uri("bolt://localhost")
             .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 将被忽略

  • 接口不创建额外的标签

如果设置了 @NodeEntity 注解的 label(如您在上面示例中看到的)或 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 上的方向属性默认为 OUTGOING。任何由 INCOMING 关系支持的字段或方法都必须明确地使用 INCOMING 方向进行注解。

使用多个相同类型的关系

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

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

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

这会工作得很好,但是请注意,这仅仅是因为终点节点类型(Car 和 Pet)是不同的类型。例如,如果您希望一个人拥有两辆汽车,那么您将不得不使用汽车的 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 或原生图 ID(Neo4j 在节点创建时分配的技术 ID)。

对于主 ID,请在任何受支持类型的字段上使用 @Id 或在提供了 AttributeConverter 的字段上使用。如果启用了索引创建,则会为此属性创建唯一索引。用户代码应在创建实体实例时手动设置 ID,或者应使用 ID 生成策略。无法存储 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 @GeneratedValue 组合描述的原生 ID。这是因为当您首次持久化实体时,由于 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 的范围应与应用程序中的“工作单元”相对应。

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

基本操作

基本操作仅限于实体上的 CRUD 操作和执行任意 Cypher 查询;无法进行更低级别的图数据库操作。

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

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

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

持久化实体

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

实体持久化通过底层 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 被持久化,但朋友、帖子或群组不会被持久化。

持久化深度
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 查询的返回子句中列出的名称。

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() 来关闭事务。

在上述示例中,仅当音乐会门票和酒店房间都可用时才提交事务,否则,不进行任何预订。

如果您不以这种方式管理事务,则会为 Session 方法(例如 saveloaddeleteexecute 等)隐式提供自动提交事务

事务默认为 READ_WRITE,但也可以以 READ_ONLY 方式打开。

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

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

原生属性类型

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

最重要的属性类型是

  • 数字

  • 字符串

  • 布尔值

  • 空间类型 Point

  • 时态类型:DateTimeLocalTimeDateTimeLocalDateTimeDuration

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

虽然在建模具有数值属性的实体时需要注意精度和比例,但数字、字符串和布尔属性的映射非常直接。然而,时态和空间类型首次出现在 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 在添加到 instants 时会考虑夏令时等。如果您确定只处理其中一种,您只需使用明确的映射到 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 设备和许多在线地图服务器使用的相同)。三维地理坐标的参考系统为 WGS-84-3D,SRID 为 4979。

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

建模领域的重要收获是,不同坐标系的点未经转换不可比较。节点的相同属性应始终使用与具有相同标签的所有其他节点相同的坐标系。否则,distance 函数和处理多个 Points 的比较将返回字面值 null

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

Neo4j-OGM 类型 Neo4j 点类型

GeographicPoint2d

一个点,在 WGS-84 地理参考系统中具有经度纬度*

GeographicPoint3d

一个点,在 WGS-84-3D 地理参考系统中具有经度纬度高度*

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 字符串,因为 Cypher 不支持字节数组

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

  • java.util.Date 转换为 ISO 8601 格式的字符串:“yyyy-MM-dd’T’HH:mm:ss.SSSXXX”(使用 DateString.ISO_8601

  • java.time.Instant 转换为带时区 ISO 8601 格式的字符串:“yyyy-MM-dd’T’HH:mm:ss.SSSZ”(使用 DateTimeFormatter.ISO_INSTANT

  • java.time.LocalDate 转换为 ISO 8601 格式的字符串:“yyyy-MM-dd”(使用 DateTimeFormatter.ISO_LOCAL_DATE

  • java.time.LocalDateTime 转换为 ISO 8601 格式的字符串:“yyyy-MM-dd’T’HH:mm:ss”(使用 DateTimeFormatter.ISO_LOCAL_DATE_TIME

  • java.time.OffsetDateTime 转换为 ISO 8601 格式的字符串:“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 提供了明确捕获每种事件类型的方法。

注册 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 接口的空操作实现。如果您不需要处理所有不同类型的持久化事件,您可以改为创建 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,它将继续响应所有持久化事件。有时您可能只想在短时间内处理事件,而不是在整个会话期间处理。

如果您已完成 EventListener 的使用,可以通过调用 session.dispose(…​) 并传入要销毁的 EventListener 来阻止它触发更多事件。

在将持久化事件分派给任何 EventListeners 之前收集这些事件的过程会给持久化层增加少量性能开销。因此,如果 Session 中没有注册 EventListeners,Neo4j-OGM 会配置为抑制事件收集阶段。在 EventListener 使用完毕后调用 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 对象,那么 DocumentFolder 都会触发事件,以反映图表中文件夹与文档之间的关系已被移除的事实。

示例:删除附加到 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 在图中不再存在,文件夹仍然持有对它的引用。与往常一样,您的代码必须负责领域模型的同步。

事件唯一性

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

示例:多次更改,每种类型一个事件
 // 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>
© . All rights reserved.