教程

简介

Neo4j-OGM Movies 是一个示例应用程序,允许您管理电影、与电影相关的个人以及评分。

虽然功能已缩减以显示使用 Neo4j-OGM 所需的步骤,但它包含现实世界应用程序中预期的所有层。

我们有意决定在这个示例中不依赖任何外部框架(如 Spring 或 Quarkus),并排除例如 Web 部分,以避免这些框架带来的额外复杂性。作为优势,我们可以更加关注 Neo4j-OGM 的行为。

该应用程序的完整源代码可在 Github 上获得。

构建域模型

在我们开始编写代码之前,我们想在白板上绘制我们的图模型。我们使用 https://arrows.app 来完成此操作。

我们的域将包含 Movies,每个电影将包含 Persons,并以不同的关系连接。我们有 ActorsDirectorsReviewers

Graph model

定义所需的域类,会产生以下代码。

class Movie {
    String title;
    List<Actor> actors;
    List<Person> directors;
    List<Reviewer> reviewers;
}

class Person {
    String name;
}

class Actor {
	List<String> roles;

	Movie movie;
	Person person;
}

class Reviewer {
	String review;
	int rating;

	Movie movie;
	Person person;
}

每当一个 Person 在一部 Movie 中演出时,他或她就会扮演一个或多个角色(roles)。为了建模这种情况,我们在模型中添加了称为关系实体的关系实体,在本例中为 Actor

我们为评论电影的 Person 准备了类似的关系表示,并将其称为 Reviewer

配置 Neo4j-OGM

Neo4j-OGM 依赖 Neo4j Java 驱动程序与数据库进行交互。驱动程序本身使用 Bolt 协议与 Neo4j 实例通信。

依赖项

要开始使用 Neo4j-OGM,我们需要将核心和 Bolt 依赖项添加到我们的项目中。即使 Neo4j-OGM 4+ 只提供通过 Bolt 连接的支持,也需要显式依赖项定义。

Neo4j-OGM 的 Maven 依赖项
<dependencies>
    <dependency>
        <groupId>org.neo4j</groupId>
        <artifactId>neo4j-ogm-core</artifactId>
        <version>4.0.11</version>
    </dependency>
    <dependency>
        <groupId>org.neo4j</groupId>
        <artifactId>neo4j-ogm-bolt-driver</artifactId>
        <version>4.0.11</version>
    </dependency>
</dependencies>

如果您使用 Gradle 或其他系统,请相应地操作。

有关依赖项的更多信息,请参阅 依赖项管理

Neo4j-OGM 配置

Neo4j-OGM 的配置分为两部分。首先,需要定义 SessionFactory 配置。在这里,您定义数据库连接。这里最常见的参数是 Neo4j uri 和凭据,您希望 Neo4j-OGM 用于连接。

第二步是应用程序特定的定义,用于指定 Neo4j-OGM 应该扫描哪些包以查找符合条件的域类。

Configuration configuration = new Configuration.Builder()
    .uri("neo4j://#:7687")
    .credentials("neo4j", "verysecret")
    .build();

SessionFactory sessionFactory = new SessionFactory(configuration, "org.neo4j.ogm.example");

注释域模型

与 Hibernate 或 JPA 非常类似,Neo4j-OGM 允许您注释 POJO,以便将它们映射到图中的节点、关系和属性。

节点实体

使用 @NodeEntity 注释的 POJO 将表示为图中的节点。

分配给该节点的标签可以通过注释上的 label 属性指定;如果未指定,它将默认为实体的简单类名。此外,每个父类还为实体贡献一个标签。当我们想要检索超类型集合时,这很有用。

让我们继续注释我们之前编写的代码中的所有节点实体。

@NodeEntity
public class Movie {
    String title;
    List<Actor> actors;
    List<Person> directors;
    List<Reviewer> reviewers;
}

@NodeEntity
public class Person {
    String name;
}

关系

接下来是节点之间的关系。

实体中每个引用另一个实体的字段都由图中的关系支持。@Relationship 注释允许您指定关系的类型和方向。默认情况下,方向假定为 OUTGOING,类型为 UPPER_SNAKE_CASE 字段名称。

我们将明确说明关系类型以避免使用默认值,并且通过不依赖字段名称使以后更容易重构类。同样,我们将修改我们在上一节中看到的代码

@NodeEntity
public class Movie {
    String title;

    @Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
    List<Actor> actors;

    @Relationship(type = "DIRECTED", direction = Relationship.Direction.INCOMING)
    List<Person> directors;

    @Relationship(type = "REVIEWED", direction = Relationship.Direction.INCOMING)
    List<Reviewer> reviewers;
}

关系实体

有时,某些东西不完全是节点实体。

在这个演示中,要注释的剩余类是 ActorReviewer。如前所述,这是一个关系实体,因为它管理 MoviePerson 之间的基础 ACTED_INREVIEWED 关系。它们不是简单关系,因为它们保存诸如 rolessummaryscore 之类的属性。

关系实体必须使用 @RelationshipEntity 注释,还需要指定关系类型。此外,它需要定义来源节点 (Person) 和目标节点 (Movie)。

我们还将向 Neo4j-OGM 指示该关系的起始节点和结束节点。

@RelationshipEntity(type = "ACTED_IN")
public class Actor {

    List<String> roles;

	@StartNode
    Person person;

    @EndNode
    Movie movie;
}

@RelationshipEntity("REVIEWED")
public class Reviewer {

    @Property("summary")
    String review;

    int rating;

    @EndNode
    Movie movie;

    @StartNode
    Person person;
}

标识符

持久化到图中的每个节点和关系都必须具有一个 id。Neo4j-OGM 使用它来识别实体并在内存中将其重新连接到图。标识符可以是主 id 或原生图 id。

  • 主 id - 任何使用 @Id 注释的属性,由用户设置,并可以选择使用 @GeneratedValue 注释

  • 原生 id - 此 id 对应于在首次保存节点或关系时由 Neo4j 数据库生成的 id,必须是 Long 类型

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

有关更多信息,请参阅 节点实体

我们的实体现在还将定义 @Id 字段。对于 Movie,我们选择了 title,对于 Person,我们选择了 name 属性。在现实世界中,名称和标题不能被视为唯一,但对于这个示例来说已经足够了。

@NodeEntity
public class Movie {
    @Id
    String title;

    @Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
    List<Actor> actors;

    @Relationship(type = "DIRECTED", direction = Relationship.Direction.INCOMING)
    List<Person> directors;

    @Relationship(type = "REVIEWED", direction = Relationship.Direction.INCOMING)
    List<Reviewer> reviewers;
}

@NodeEntity
public class Person {
    @Id
    String name;
}

此外,我们还将生成的内部 id 引用添加到关系实体。

@RelationshipEntity(type = "ACTED_IN")
public class Actor {

    @Id @GeneratedValue
    Long id;

    List<String> roles;

    @StartNode
    Person person;

    @EndNode
    Movie movie;
}

@RelationshipEntity("REVIEWED")
public class Reviewer {

    @Id @GeneratedValue
    Long id;

    @Property("summary")
    String review;

    int rating;

    @EndNode
    Movie movie;

    @StartNode
    Person person;
}

无参数构造函数

我们快到了!

Neo4j-OGM 还需要一个公共无参数构造函数才能从所有注释实体中构建对象。由于我们没有定义任何自定义构造函数,因此我们可以继续。

与模型交互

因此,我们的域实体已进行注释,现在我们准备将它们持久化到图中!

会话

智能对象映射功能由 Session 对象提供。Session 来自 SessionFactory

我们将只设置一次 `SessionFactory` 一次,并让它生成所需数量的会话。

Session 会跟踪对实体和关系所做的更改,并在保存时持久化已修改的实体。一旦实体被会话跟踪,在同一会话范围内重新加载该实体将导致会话缓存返回先前加载的实体。但是,如果实体或其相关实体从图中检索到更多关系,则会话中的子图将扩展。

在本示例应用程序中,我们将使用短生命周期会话 - 每个操作一个新会话 - 以避免陈旧数据问题。

我们的示例应用程序将使用以下操作

public class MovieService {

    Movie findMovieByTitle(String title) {
        // implementation
    }

    List<Movie> allMovies() {
        // implementation
    }

    Movie updateTagline(String title, String newTagline) {
        // implementation
    }
}

所有这些与图的 CRUD 交互都由 `Session` 处理。每当我们想要运行一个工作单元时,我们都会打开会话并使用同一个 `Session` 实例执行所有操作。让我们看一下服务的实现(以及带有 `SessionFactory` 实例化的构造函数)

public class MovieService {

    final SessionFactory sessionFactory;

    public MovieService() {
        Configuration config = new Configuration.Builder()
            .uri("neo4j://#:7687")
            .credentials("neo4j", "verysecret")
            .build();
        this.sessionFactory = new SessionFactory(config, "org.neo4j.ogm.example");
    }

    Movie findMovieByTitle(String title) {
        Session session = sessionFactory.openSession();
        return Optional.ofNullable(
                session.queryForObject(Movie.class, "MATCH (m:Movie {title:$title}) return m", Map.of("title", title)))
            .orElseThrow(() -> new MovieNotFoundException(title));
    }

    public List<Movie> allMovies() {
        Session session = sessionFactory.openSession();
        return new ArrayList<>(session.loadAll(Movie.class));
    }

    Movie updateTagline(String title, String newTagline) {
        Session session = sessionFactory.openSession();
        Movie movie = session.queryForObject(Movie.class, "MATCH (m:Movie{title:$title}) return m", Map.of("title", title));
        Movie updatedMovie = movie.withTagline(newTagline);
        session.save(updatedMovie);
        return updatedMovie;
    }
}

如您在 `updateTagline` 方法中所见,`Session` 在一开始就被打开,以便在同一个实例中进行加载和持久化操作。

当服务获取所有 `Movies` 时,默认加载深度为 `1` 将应用于操作。较高的值对我们的示例没有影响,因为我们只加载 `Movies` 及其直接邻居。有关 加载深度 的章节提供了有关关系遍历的默认行为的更多见解。

自定义查询和映射

如您所见,我们已经使用 `Session#query` 的自定义查询来获取标题为 `Movie` 的电影。有时您最终会从查询中返回数据,这些数据不适合您之前定义的任何实体。

假设我们想收集与所有评论者一起加入的 `Movie` 的平均评分。当然,我们也可以在应用程序本身中执行此操作,但在数据库端准备数据不仅可以减少通过网络传输的数据量,而且在大多数情况下,数据库执行此操作的速度要快得多。

public record MovieRating(String title, float rating, List<String> reviewers) {
}

在我们的服务中添加另一个方法,包括自定义查询结果

List<MovieRating> getRatings() {
    Session session = sessionFactory.openSession();
    List<MovieRating> ratings = session.queryDto(
        "MATCH (m:Movie)<-[r:REVIEWED]-(p:Person) RETURN m.title as title, avg(r.rating) as rating, collect(p.name) as reviewers",
        Map.of(),
        MovieRating.class);
    return ratings;
}

结论

我们几乎不费吹灰之力就构建了将此应用程序联系在一起的所有服务。所需要的只是添加控制器并构建 UI。完整的应用程序可以在 Github 上找到。

我们鼓励您阅读后续的参考指南,并通过分叉应用程序并添加内容来应用所学概念。