教程

简介

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

尽管功能已精简,以展示使用 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

我们为评审了 MoviePerson 准备了类似的关系表示,并称之为 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.18</version>
    </dependency>
    <dependency>
        <groupId>org.neo4j</groupId>
        <artifactId>neo4j-ogm-bolt-driver</artifactId>
        <version>4.0.18</version>
    </dependency>
</dependencies>

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

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

Neo4j-OGM 配置

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

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

Configuration configuration = new Configuration.Builder()
    .uri("neo4j://localhost: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 - 任何使用 @Id 注解的属性,由用户设置,可选地带有 @GeneratedValue 注解

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

请勿 在长时间运行的应用程序中依赖原生 ID。Neo4j 会重用已删除的节点 ID。建议用户为其领域对象创建自己的唯一标识符(或使用 UUID)。

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

我们的实体现在也将定义 @Id 字段。对于 Movie,我们选择了 标题 属性,对于 Person,选择了 名称 属性。在实际场景中,名称和标题不能被视为唯一,但对于本示例来说已经足够。

@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://localhost: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 上获取。

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

© . All rights reserved.