参考

简介

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 提供了一个会话,用于与映射的实体和 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

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

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

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

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

Gradle

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

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

配置

配置方法

有几种方法可以为 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://localhost")
             .credentials("user", "password")
             .build()

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

传输层安全 (TLS/SSL)

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

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

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

这两种策略都会使您容易受到中间人攻击。除非您的服务器位于安全防火墙之后,否则您可能不应该使用它们。
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 label 属性的内容,或者如果未指定,则默认为实体的简单类名。有时可能需要在运行时向节点添加和删除其他标签。我们可以使用 @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;
...
}

这将正常工作,但是,请注意,这仅仅是因为端节点类型(汽车和宠物)是不同类型。例如,如果您想让一个人拥有两辆汽车,那么您必须使用汽车的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,请在任何受支持类型的字段或具有提供的AttributeConverter的字段上使用@Id。为此属性创建唯一索引(如果启用了索引创建)。用户代码应在创建实体实例时手动设置 ID,或者应使用 ID 生成策略。无法存储 ID 值为 null 且没有生成策略的实体。

在关联实体上指定主 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,并且实体只能包含一个此类字段。

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

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

  • 保存对象时,version 字段由 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 处理的实体必须有一个空的公共构造函数,以允许库构造对象。

除非使用注解另行指定,否则框架将尝试将对象的任何“简单”字段映射到节点属性,并将任何丰富的复合对象映射到相关节点。“简单”字段是指任何基本类型、包装基本类型或字符串或其数组,基本上是任何自然适合 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实体,并为您管理事务处理和异常转换。检索对象的渴望程度通过为任何加载方法指定深度参数来控制。

实体持久化是通过底层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查询的返回子句中列出的名称。

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-Driver手册

在对具有数字属性的实体进行建模时(关于精度和比例),您需要稍微注意一下,但数字、字符串和布尔属性的映射非常简单。然而,时间和空间类型在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 设备和许多在线地图服务器使用的相同系统)。三维地理坐标具有 WGS-84-3D 参考系统,SRID 为 4979。

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

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

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

Neo4j-OGM类型 Neo4j 点类型

GeographicPoint2d

一个具有 longitudelatitude 的点,位于 WGS-84 的地理参考系统中*

GeographicPoint3d

一个具有 longitudelatitudeheight 的点,位于 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)的对象到字符串属性

  • 二进制数据(作为 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

它们需要应用于属性以获得自定义字符串格式,或者在您希望将日期或日期时间值存储为长整型时使用

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

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

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

存储为长整型值的日期示例
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之前收集持久化事件的过程会给持久化层增加少量性能开销。因此,Neo4j-OGM配置为如果Session中没有注册EventListeners,则抑制事件收集阶段。在完成 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对象,则将为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的引用,即使它在图中不再存在。与往常一样,您的代码必须负责域模型同步。

事件唯一性

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

示例:多次更改,每种类型的单个事件
 // 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);

测试

在测试方面有几种选择。您可以选择通过测试框架使用嵌入式实例,或者使用Testcontainers Neo4j等外部库。

测试框架

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

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

  • 声明Neo4jRule JUnit 规则,以设置 Neo4j 测试服务器(JUnit4 和此规则对于运行测试框架不是必需的)

  • 设置 Neo4j-OGM 配置和SessionFactory

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

日志级别

运行单元测试时,查看 Neo4j-OGM 正在执行的操作,尤其是查看在应用程序和数据库之间传输的 Cypher 请求,会很有帮助。Neo4j-OGM 使用slf4j以及Logback作为其日志框架,默认情况下,所有 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>