15_spring_data_neo4j简单教程

KnowledgeGraph

教程代码示例

https://gitee.com/suveng/graph-movie-app

Spring Data Neo4j 简单教程

简介

Spring Data Neo4j 是 Spring Data 项目的一部分,它提供了对 Neo4j 图数据库的集成支持。通过 Spring Data Neo4j,开发者可以轻松地在 Spring Boot 应用中使用 Neo4j 数据库,利用图数据库的优势处理复杂的关系数据。

本教程基于一个完整的电影图数据库应用,展示如何使用 Spring Data Neo4j 构建图数据库应用程序。

环境准备

1. 安装 Neo4j

首先需要安装 Neo4j 数据库。可以通过以下方式安装:

  • Docker 方式:

docker run \

    --publish=7474:7474 --publish=7687:7687 \

    --env NEO4J_AUTH=neo4j/your_password \

    --env NEO4J_PLUGINS=["apoc"] \

    neo4j:5.15.0-community

注意:

  • 使用 neo4j:5.15.0-community 指定了具体的Neo4j版本(社区版)

  • 推荐使用稳定版本,避免使用 latest 标签,以确保环境一致性

  • 如果需要企业版功能,可以使用 neo4j:5.15.0-enterprise

  • 此命令不包含数据卷挂载,容器删除后数据会丢失。如需持久化数据,可以添加 --volume 参数

Neo4j社区版与企业版的区别

Neo4j提供两个主要版本:社区版(Community Edition)和企业版(Enterprise Edition),它们有以下主要区别:

社区版(Community Edition)

  • 免费使用:完全免费,适合开发、测试和小规模生产环境

  • 核心功能:包含完整的图数据库核心功能

  • 基础安全:提供基本的认证和授权功能

  • 单机部署:支持单实例部署

  • 基础备份:提供基本的备份和恢复功能

  • 适用场景:个人项目、学习、开发环境、小型应用

企业版(Enterprise Edition)

  • 商业许可:需要购买商业许可证

  • 高级安全:提供LDAP/Active Directory集成、Kerberos认证、SSL/TLS加密等

  • 集群支持:支持因果集群(Causal Clustering),实现高可用性和水平扩展

  • 高级备份:提供在线备份、增量备份和更强大的恢复功能

  • 监控和管理:包含更详细的监控指标和管理工具

  • 性能优化:包含更多性能优化功能

  • 适用场景:大型企业应用、高可用性要求的生产环境、需要处理大规模数据的场景

选择建议

  • 对于学习、开发和小型项目,社区版已经足够

  • 对于需要高可用性、高级安全功能或大规模部署的生产环境,建议使用企业版

  • 可以先使用社区版进行开发,后续根据需要升级到企业版

注意:

  • NEO4J_AUTH=neo4j/your_password 设置用户名和密码,请将 your_password 替换为您自己的密码

  • NEO4J_PLUGINS=["apoc"] 可选,安装APOC插件以提供更多功能

  • 首次启动时,Neo4j会使用提供的凭据创建用户

关于APOC插件

APOC (Awesome Procedures On Cypher) 是 Neo4j 的一个扩展插件,提供了许多额外的函数和过程,大大增强了 Neo4j 的功能。APOC插件包含:

  1. 数据处理功能:JSON处理、XML处理、日期时间操作等

  2. 图算法:最短路径、中心性算法、社区检测等

  3. 数据导入导出:从各种数据源(CSV、JSON、XML、数据库等)导入数据

  4. 图重构:节点合并、关系合并、图变换等

  5. 元数据操作:索引管理、约束管理、模式信息查询等

  6. 虚拟关系和节点:动态创建不实际存储在数据库中的关系和节点

APOC插件是可选的,但对于生产环境和复杂应用场景非常有用。如果您不需要这些额外功能,可以移除 NEO4J_PLUGINS=["apoc"] 环境变量。

  • 官方下载安装:访问 Neo4j 官网 下载对应平台的安装包

2. 创建 Spring Boot 项目

创建一个新的 Spring Boot 项目,推荐使用 Spring Boot 3.x 版本(如 3.2.0)以确保与 Neo4j 5.x 的兼容性。

pom.xml 中添加以下依赖:


<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-parent</artifactId>

        <version>3.2.0</version>

        <relativePath/> <!-- lookup parent from repository -->

    </parent>

  

    <groupId>com.example</groupId>

    <artifactId>graph-movie-app</artifactId>

    <version>0.0.1-SNAPSHOT</version>

    <name>graph-movie-app</name>

    <description>Spring Boot Neo4j Movie Graph Application</description>

  

    <properties>

        <java.version>17</java.version>

        <testcontainers.version>1.19.3</testcontainers.version>

    </properties>

  

    <dependencies>

        <!-- Spring Boot Neo4j Starter -->

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-data-neo4j</artifactId>

        </dependency>

  

        <!-- Spring Boot Web Starter -->

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

  

        <!-- Spring Boot Validation Starter -->

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-validation</artifactId>

        </dependency>

  

        <!-- Spring Boot Test Starter -->

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-test</artifactId>

            <scope>test</scope>

        </dependency>

  

        <!-- Testcontainers Neo4j -->

        <dependency>

            <groupId>org.testcontainers</groupId>

            <artifactId>neo4j</artifactId>

            <version>${testcontainers.version}</version>

            <scope>test</scope>

        </dependency>

  

        <!-- Testcontainers JUnit Jupiter -->

        <dependency>

            <groupId>org.testcontainers</groupId>

            <artifactId>junit-jupiter</artifactId>

            <version>${testcontainers.version}</version>

            <scope>test</scope>

        </dependency>

  

        <!-- JUnit Jupiter -->

        <dependency>

            <groupId>org.junit.jupiter</groupId>

            <artifactId>junit-jupiter</artifactId>

            <scope>test</scope>

        </dependency>

  

        <!-- Lombok (Optional) -->

        <dependency>

            <groupId>org.projectlombok</groupId>

            <artifactId>lombok</artifactId>

            <optional>true</optional>

        </dependency>

  

        <!-- Fastjson for JSON serialization -->

        <dependency>

            <groupId>com.alibaba.fastjson2</groupId>

            <artifactId>fastjson2</artifactId>

            <version>2.0.43</version>

        </dependency>

  

        <dependency>

            <groupId>com.alibaba.fastjson2</groupId>

            <artifactId>fastjson2-extension-spring6</artifactId>

            <version>2.0.43</version>

        </dependency>

    </dependencies>

  

    <dependencyManagement>

        <dependencies>

            <dependency>

                <groupId>org.testcontainers</groupId>

                <artifactId>testcontainers-bom</artifactId>

                <version>${testcontainers.version}</version>

                <type>pom</type>

                <scope>import</scope>

            </dependency>

        </dependencies>

    </dependencyManagement>

  

    <build>

        <plugins>

            <plugin>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-maven-plugin</artifactId>

                <configuration>

                    <excludes>

                        <exclude>

                            <groupId>org.projectlombok</groupId>

                            <artifactId>lombok</artifactId>

                        </exclude>

                    </excludes>

                </configuration>

            </plugin>

        </plugins>

    </build>

</project>

测试依赖说明

  1. spring-boot-starter-test

- Spring Boot 的核心测试启动器

- 包含了大多数测试所需的库,如 JUnit 5、Mockito、AssertJ 等

- 提供了 Spring Boot 测试上下文支持

  1. junit-jupiter

- JUnit 5 的核心模块

- 提供了现代的测试框架和注解(@Test、@BeforeEach、@AfterEach 等)

- 支持参数化测试和动态测试

  1. mockito-core

- 强大的模拟框架

- 用于创建和管理模拟对象

- 支持 BDD(行为驱动开发)风格的测试

  1. testcontainers

- 提供轻量级的、一次性的数据库实例

- 用于集成测试,确保测试环境的一致性

- 支持多种数据库,包括 Neo4j

  1. testcontainers-neo4j

- TestContainers 的 Neo4j 模块

- 专门用于在测试中启动和管理 Neo4j 容器

- 确保每个测试都有干净的数据库环境

测试配置

src/test/resources/application-test.yml 中添加测试配置:


spring:

  neo4j:

    uri: bolt://localhost:7687

    authentication:

      username: neo4j

      password: test_password

logging:

  level:

    org.springframework.data.neo4j: DEBUG

使用 TestContainers 进行集成测试

如果需要使用 TestContainers 进行集成测试,可以创建以下测试基类:


import org.junit.jupiter.api.BeforeAll;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.DynamicPropertyRegistry;

import org.springframework.test.context.DynamicPropertySource;

import org.testcontainers.containers.Neo4jContainer;

import org.testcontainers.junit.jupiter.Container;

import org.testcontainers.junit.jupiter.Testcontainers;

  

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

@Testcontainers

public abstract class Neo4jIntegrationTestBase {

    @Container

    static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5.15.0-community")

            .withAdminPassword("test_password");

    @DynamicPropertySource

    static void neo4jProperties(DynamicPropertyRegistry registry) {

        registry.add("spring.neo4j.uri", neo4jContainer::getBoltUrl);

        registry.add("spring.neo4j.authentication.username", () -> "neo4j");

        registry.add("spring.neo4j.authentication.password", () -> "test_password");

    }

}

然后集成测试类可以继承这个基类:


public class MovieIntegrationTest extends Neo4jIntegrationTestBase {

    // 测试代码...

}

版本兼容性说明

  • Spring Boot 3.x 与 Neo4j 5.x 兼容性最佳

  • 如果使用 Spring Boot 2.x,建议使用 Neo4j 4.x 版本

  • Spring Boot 3.x 需要 Java 17 或更高版本

配置连接

application.propertiesapplication.yml 中配置 Neo4j 连接信息:


# Neo4j 连接配置

spring.neo4j.uri=bolt://localhost:7687

spring.neo4j.authentication.username=neo4j

spring.neo4j.authentication.password=your_password

或者使用 YAML 格式:


spring:

  neo4j:

    uri: bolt://localhost:7687

    authentication:

      username: neo4j

      password: your_password

定义节点实体

使用 @Node 注解定义图数据库中的节点实体。项目中的实体类使用了更多注解和验证功能:

Person 实体类


package com.example.graphmovieapp.domain.entity;

  

import org.springframework.data.neo4j.core.schema.Id;

import org.springframework.data.neo4j.core.schema.Node;

import org.springframework.data.neo4j.core.schema.Property;

import org.springframework.data.neo4j.core.schema.Relationship;

import org.springframework.data.neo4j.core.schema.GeneratedValue;

import com.alibaba.fastjson2.annotation.JSONField;

  

import jakarta.validation.constraints.NotBlank;

import jakarta.validation.constraints.Min;

import jakarta.validation.constraints.Max;

import java.util.ArrayList;

import java.util.List;

import java.util.Objects;

  

/**

 * Person实体类,表示图数据库中的人物节点

 */

@Node("Person")

public class Person {

  

    /**

     * 人物ID,主键

     */

    @Id

    @GeneratedValue

    private Long id;

  

    /**

     * 人物姓名

     */

    @Property("name")

    @NotBlank(message = "人物姓名不能为空")

    private String name;

  

    /**

     * 出生年份

     */

    @Property("born")

    @Min(value = 1800, message = "出生年份不能早于1800年")

    @Max(value = 2100, message = "出生年份不能晚于2100年")

    private Integer born;

  

    /**

     * 参演的电影列表(ACTED_IN关系)

     */

    @Relationship(type = "ACTED_IN", direction = Relationship.Direction.OUTGOING)

    @JSONField(serialize = false, deserialize = false)

    private List<Movie> actedInMovies = new ArrayList<>();

  

    /**

     * 执导的电影列表(DIRECTED关系)

     */

    @Relationship(type = "DIRECTED", direction = Relationship.Direction.OUTGOING)

    @JSONField(serialize = false, deserialize = false)

    private List<Movie> directedMovies = new ArrayList<>();

  

    /**

     * 默认构造函数

     */

    public Person() {

    }

  

    /**

     * 带参数的构造函数

     */

    public Person(String name, Integer born) {

        this.name = name;

        this.born = born;

    }

  

    // getter和setter方法...

  

    /**

     * 添加参演的电影

     */

    public void addActedInMovie(Movie movie) {

        if (!actedInMovies.contains(movie)) {

            actedInMovies.add(movie);

        }

    }

  

    /**

     * 移除参演的电影

     */

    public void removeActedInMovie(Movie movie) {

        actedInMovies.remove(movie);

    }

  

    /**

     * 添加执导的电影

     */

    public void addDirectedMovie(Movie movie) {

        if (!directedMovies.contains(movie)) {

            directedMovies.add(movie);

        }

    }

  

    /**

     * 移除执导的电影

     */

    public void removeDirectedMovie(Movie movie) {

        directedMovies.remove(movie);

    }

  

    // equals、hashCode和toString方法...

}

Movie 实体类


package com.example.graphmovieapp.domain.entity;

  

import org.springframework.data.neo4j.core.schema.Id;

import org.springframework.data.neo4j.core.schema.Node;

import org.springframework.data.neo4j.core.schema.Property;

import org.springframework.data.neo4j.core.schema.Relationship;

import org.springframework.data.neo4j.core.schema.GeneratedValue;

import com.alibaba.fastjson2.annotation.JSONField;

  

import jakarta.validation.constraints.NotBlank;

import jakarta.validation.constraints.Min;

import jakarta.validation.constraints.Max;

import java.util.ArrayList;

import java.util.List;

import java.util.Objects;

  

/**

 * Movie实体类,表示图数据库中的电影节点

 */

@Node("Movie")

public class Movie {

  

    /**

     * 电影ID,主键

     */

    @Id

    @GeneratedValue

    private Long id;

  

    /**

     * 电影标题

     */

    @Property("title")

    @NotBlank(message = "电影标题不能为空")

    private String title;

  

    /**

     * 发行年份

     */

    @Property("released")

    @Min(value = 1800, message = "发行年份不能早于1800年")

    @Max(value = 2100, message = "发行年份不能晚于2100年")

    private Integer released;

  

    /**

     * 演员列表(ACTED_IN关系的入向)

     */

    @Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)

    @JSONField(serialize = false, deserialize = false)

    private List<Person> actors = new ArrayList<>();

  

    /**

     * 导演列表(DIRECTED关系的入向)

     */

    @Relationship(type = "DIRECTED", direction = Relationship.Direction.INCOMING)

    @JSONField(serialize = false, deserialize = false)

    private List<Person> directors = new ArrayList<>();

  

    /**

     * 默认构造函数

     */

    public Movie() {

    }

  

    /**

     * 带参数的构造函数

     */

    public Movie(String title, Integer released) {

        this.title = title;

        this.released = released;

    }

  

    // getter和setter方法...

  

    /**

     * 添加演员

     */

    public void addActor(Person actor) {

        if (!actors.contains(actor)) {

            actors.add(actor);

        }

    }

  

    /**

     * 移除演员

     */

    public void removeActor(Person actor) {

        actors.remove(actor);

    }

  

    /**

     * 添加导演

     */

    public void addDirector(Person director) {

        if (!directors.contains(director)) {

            directors.add(director);

        }

    }

  

    /**

     * 移除导演

     */

    public void removeDirector(Person director) {

        directors.remove(director);

    }

  

    // equals、hashCode和toString方法...

}

创建仓库接口

创建继承自 Neo4jRepository 的仓库接口,提供丰富的数据访问方法:

PersonRepository


package com.example.graphmovieapp.repository;

  

import com.example.graphmovieapp.domain.entity.Person;

import com.example.graphmovieapp.domain.entity.Movie;

import org.springframework.data.neo4j.repository.Neo4jRepository;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.repository.query.Param;

import org.springframework.data.neo4j.repository.query.Query;

import org.springframework.stereotype.Repository;

  

import java.util.List;

import java.util.Optional;

  

/**

 * PersonRepository接口,提供对Person实体的数据访问操作

 */

@Repository

public interface PersonRepository extends Neo4jRepository<Person, Long> {

  

    /**

     * 按姓名查找人物

     */

    Optional<Person> findByName(String name);

  

    /**

     * 按出生年份查找人物

     */

    List<Person> findByBorn(Integer born);

  

    /**

     * 按姓名模糊查询人物

     */

    List<Person> findByNameContaining(String name);

  

    /**

     * 按姓名模糊查询人物(支持分页)

     */

    Page<Person> findByNameContaining(String name, Pageable pageable);

  

    /**

     * 查找指定演员参演的所有电影

     */

    @Query("MATCH (p:Person {name: $name})-[:ACTED_IN]->(m:Movie) RETURN m")

    List<Movie> findMoviesActedByPerson(@Param("name") String name);

  

    /**

     * 查找指定演员参演的所有电影(支持分页)

     */

    @Query(value = "MATCH (p:Person {name: $name})-[:ACTED_IN]->(m:Movie) RETURN m ORDER BY m.title SKIP $skip LIMIT $limit",

           countQuery = "MATCH (p:Person {name: $name})-[:ACTED_IN]->(m:Movie) RETURN count(m)")

    Page<Movie> findMoviesActedByPerson(@Param("name") String name, Pageable pageable);

  

    /**

     * 查找指定导演执导的所有电影

     */

    @Query("MATCH (p:Person {name: $name})-[:DIRECTED]->(m:Movie) RETURN m")

    List<Movie> findMoviesDirectedByPerson(@Param("name") String name);

  

    /**

     * 查找参演指定电影的演员

     */

    @Query("MATCH (p:Person)-[:ACTED_IN]->(m:Movie {title: $title}) RETURN p")

    List<Person> findActorsInMovie(@Param("title") String title);

  

    /**

     * 查找参演指定电影的演员(支持分页)

     */

    @Query(value = "MATCH (p:Person)-[:ACTED_IN]->(m:Movie {title: $title}) RETURN p ORDER BY p.name SKIP $skip LIMIT $limit",

           countQuery = "MATCH (p:Person)-[:ACTED_IN]->(m:Movie {title: $title}) RETURN count(p)")

    Page<Person> findActorsInMovie(@Param("title") String title, Pageable pageable);

  

    /**

     * 查找指定电影的导演

     */

    @Query("MATCH (p:Person)-[:DIRECTED]->(m:Movie {title: $title}) RETURN p")

    List<Person> findDirectorsOfMovie(@Param("title") String title);

}

MovieRepository


package com.example.graphmovieapp.repository;

  

import com.example.graphmovieapp.domain.entity.Movie;

import com.example.graphmovieapp.domain.entity.Person;

import org.springframework.data.neo4j.repository.Neo4jRepository;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.repository.query.Param;

import org.springframework.data.neo4j.repository.query.Query;

import org.springframework.stereotype.Repository;

  

import java.util.List;

import java.util.Optional;

  

/**

 * MovieRepository接口,提供对Movie实体的数据访问操作

 */

@Repository

public interface MovieRepository extends Neo4jRepository<Movie, Long> {

  

    /**

     * 按标题查找电影

     */

    Optional<Movie> findByTitle(String title);

  

    /**

     * 按发行年份查找电影

     */

    List<Movie> findByReleased(Integer released);

  

    /**

     * 按标题模糊查询电影

     */

    List<Movie> findByTitleContaining(String title);

  

    /**

     * 按标题模糊查询电影(支持分页)

     */

    @Query(value = "MATCH (m:Movie) WHERE m.title CONTAINS $title RETURN m ORDER BY m.title SKIP $skip LIMIT $limit",

           countQuery = "MATCH (m:Movie) WHERE m.title CONTAINS $title RETURN count(m)")

    Page<Movie> findByTitleContaining(@Param("title") String title, Pageable pageable);

  

    /**

     * 按发行年份范围查找电影

     */

    List<Movie> findByReleasedBetween(Integer start, Integer end);

  

    /**

     * 按发行年份范围查找电影(支持分页)

     */

    @Query(value = "MATCH (m:Movie) WHERE $start <= m.released <= $end RETURN m ORDER BY m.released SKIP $skip LIMIT $limit",

           countQuery = "MATCH (m:Movie) WHERE $start <= m.released <= $end RETURN count(m)")

    Page<Movie> findByReleasedBetween(@Param("start") Integer start, @Param("end") Integer end, Pageable pageable);

  

    /**

     * 查找指定演员参演的电影数量

     */

    @Query("MATCH (p:Person {name: $name})-[:ACTED_IN]->(m:Movie) RETURN count(m)")

    Long countMoviesActedByPerson(@Param("name") String name);

  

    /**

     * 查找同时担任演员和导演的电影

     */

    @Query("MATCH (p:Person {name: $name})-[:ACTED_IN]->(m:Movie) WHERE (p)-[:DIRECTED]->(m) RETURN m")

    List<Movie> findMoviesActedAndDirectedByPerson(@Param("name") String name);

  

    /**

     * 查找合作演员(参演同一电影的不同演员)

     */

    @Query("MATCH (p1:Person {name: $name})-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) WHERE p1 <> p2 RETURN DISTINCT p2")

    List<Person> findCoActors(@Param("name") String name);

}

创建服务层

创建服务层接口和实现类来处理业务逻辑:

MovieService 接口


package com.example.graphmovieapp.service;

  

import com.example.graphmovieapp.domain.entity.Movie;

import com.example.graphmovieapp.domain.entity.Person;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

  

import java.util.List;

import java.util.Optional;

  

/**

 * MovieService接口,提供电影相关的业务操作

 */

public interface MovieService {

  

    /**

     * 保存电影

     */

    Movie saveMovie(Movie movie);

  

    /**

     * 根据ID查找电影

     */

    Optional<Movie> findMovieById(Long id);

  

    /**

     * 查找所有电影

     */

    List<Movie> findAllMovies();

  

    /**

     * 根据ID删除电影

     */

    void deleteMovie(Long id);

  

    /**

     * 根据标题查找电影

     */

    Optional<Movie> findMovieByTitle(String title);

  

    /**

     * 根据发行年份查找电影

     */

    List<Movie> findMoviesByReleased(Integer released);

  

    /**

     * 根据标题模糊查询电影(支持分页)

     */

    Page<Movie> findMoviesByTitleContaining(String title, Pageable pageable);

  

    /**

     * 根据发行年份范围查找电影

     */

    List<Movie> findMoviesByReleasedBetween(Integer start, Integer end);

  

    /**

     * 查找指定演员参演的电影数量

     */

    Long countMoviesActedByPerson(String name);

  

    /**

     * 查找同时担任演员和导演的电影

     */

    List<Movie> findMoviesActedAndDirectedByPerson(String name);

  

    /**

     * 查找合作演员

     */

    List<Person> findCoActors(String name);

  

    /**

     * 创建电影并添加演员和导演

     */

    Movie createMovieWithActorsAndDirectors(Movie movie, List<String> actorNames, List<String> directorNames);

}

MovieService 实现类


package com.example.graphmovieapp.service.impl;

  

import com.example.graphmovieapp.domain.entity.Movie;

import com.example.graphmovieapp.domain.entity.Person;

import com.example.graphmovieapp.repository.MovieRepository;

import com.example.graphmovieapp.repository.PersonRepository;

import com.example.graphmovieapp.service.MovieService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

  

import java.util.List;

import java.util.Optional;

  

/**

 * MovieService实现类,提供电影相关的业务操作

 */

@Service

@Transactional

public class MovieServiceImpl implements MovieService {

  

    private final MovieRepository movieRepository;

    private final PersonRepository personRepository;

  

    /**

     * 构造函数,注入Repository依赖

     */

    @Autowired

    public MovieServiceImpl(MovieRepository movieRepository, PersonRepository personRepository) {

        this.movieRepository = movieRepository;

        this.personRepository = personRepository;

    }

  

    /**

     * 保存电影

     */

    @Override

    @Transactional

    public Movie saveMovie(Movie movie) {

        if (movie == null) {

            throw new IllegalArgumentException("电影不能为空");

        }

        return movieRepository.save(movie);

    }

  

    /**

     * 根据ID查找电影

     */

    @Override

    @Transactional(readOnly = true)

    public Optional<Movie> findMovieById(Long id) {

        if (id == null) {

            throw new IllegalArgumentException("电影ID不能为空");

        }

        return movieRepository.findById(id);

    }

  

    /**

     * 查找所有电影

     */

    @Override

    @Transactional(readOnly = true)

    public List<Movie> findAllMovies() {

        return movieRepository.findAll();

    }

  

    /**

     * 创建电影并添加演员和导演

     */

    @Override

    @Transactional

    public Movie createMovieWithActorsAndDirectors(Movie movie, List<String> actorNames, List<String> directorNames) {

        if (movie == null) {

            throw new IllegalArgumentException("电影不能为空");

        }

        if (movie.getTitle() == null || movie.getTitle().trim().isEmpty()) {

            throw new IllegalArgumentException("电影标题不能为空");

        }

  

        // 保存电影

        Movie savedMovie = movieRepository.save(movie);

  

        // 添加演员

        if (actorNames != null && !actorNames.isEmpty()) {

            for (String actorName : actorNames) {

                if (actorName != null && !actorName.trim().isEmpty()) {

                    Optional<Person> actorOpt = personRepository.findByName(actorName);

                    if (actorOpt.isPresent()) {

                        Person actor = actorOpt.get();

                        actor.addActedInMovie(savedMovie);

                        savedMovie.addActor(actor);

                        personRepository.save(actor);

                    } else {

                        // 如果演员不存在,创建一个新的演员记录

                        Person newActor = new Person(actorName, null);

                        newActor.addActedInMovie(savedMovie);

                        savedMovie.addActor(newActor);

                        personRepository.save(newActor);

                    }

                }

            }

        }

  

        // 添加导演

        if (directorNames != null && !directorNames.isEmpty()) {

            for (String directorName : directorNames) {

                if (directorName != null && !directorName.trim().isEmpty()) {

                    Optional<Person> directorOpt = personRepository.findByName(directorName);

                    if (directorOpt.isPresent()) {

                        Person director = directorOpt.get();

                        director.addDirectedMovie(savedMovie);

                        savedMovie.addDirector(director);

                        personRepository.save(director);

                    } else {

                        // 如果导演不存在,创建一个新的导演记录

                        Person newDirector = new Person(directorName, null);

                        newDirector.addDirectedMovie(savedMovie);

                        savedMovie.addDirector(newDirector);

                        personRepository.save(newDirector);

                    }

                }

            }

        }

  

        return movieRepository.save(savedMovie);

    }

}

创建控制器

创建 REST API 控制器来提供 HTTP 接口:


package com.example.graphmovieapp.controller;

  

import com.example.graphmovieapp.domain.entity.Movie;

import com.example.graphmovieapp.domain.entity.Person;

import com.example.graphmovieapp.service.MovieService;

import com.example.graphmovieapp.service.PersonService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.PageRequest;

import org.springframework.data.domain.Pageable;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;

  

import java.util.List;

import java.util.Map;

import java.util.HashMap;

import java.util.Optional;

  

/**

 * MovieController类,提供电影相关的REST API端点

 */

@RestController

@RequestMapping("/api/movies")

public class MovieController {

  

    private final MovieService movieService;

    private final PersonService personService;

  

    /**

     * 构造函数,注入Service依赖

     */

    @Autowired

    public MovieController(MovieService movieService, PersonService personService) {

        this.movieService = movieService;

        this.personService = personService;

    }

  

    /**

     * 创建电影

     */

    @PostMapping

    public ResponseEntity<Movie> createMovie(@Valid @RequestBody Movie movie) {

        try {

            Movie savedMovie = movieService.saveMovie(movie);

            return new ResponseEntity<>(savedMovie, HttpStatus.CREATED);

        } catch (IllegalArgumentException e) {

            return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);

        }

    }

  

    /**

     * 根据ID获取电影

     */

    @GetMapping("/{id}")

    public ResponseEntity<Movie> getMovieById(@PathVariable Long id) {

        try {

            Optional<Movie> movie = movieService.findMovieById(id);

            return movie.map(value -> new ResponseEntity<>(value, HttpStatus.OK))

                    .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));

        } catch (IllegalArgumentException e) {

            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        }

    }

  

    /**

     * 获取所有电影

     */

    @GetMapping

    public ResponseEntity<List<Movie>> getAllMovies() {

        List<Movie> movies = movieService.findAllMovies();

        return new ResponseEntity<>(movies, HttpStatus.OK);

    }

  

    /**

     * 删除电影

     */

    @DeleteMapping("/{id}")

    public ResponseEntity<Void> deleteMovie(@PathVariable Long id) {

        try {

            movieService.deleteMovie(id);

            return new ResponseEntity<>(HttpStatus.NO_CONTENT);

        } catch (IllegalArgumentException e) {

            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        }

    }

  

    /**

     * 根据标题获取电影

     */

    @GetMapping("/by-title")

    public ResponseEntity<Movie> getMovieByTitle(@RequestParam String title) {

        try {

            Optional<Movie> movie = movieService.findMovieByTitle(title);

            return movie.map(value -> new ResponseEntity<>(value, HttpStatus.OK))

                    .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));

        } catch (IllegalArgumentException e) {

            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        }

    }

  

    /**

     * 标题模糊查询(分页)

     */

    @GetMapping("/search")

    public ResponseEntity<Page<Movie>> searchMoviesByTitle(

            @RequestParam String title,

            @RequestParam(defaultValue = "0") int page,

            @RequestParam(defaultValue = "10") int size) {

        try {

            Pageable pageable = PageRequest.of(page, size);

            Page<Movie> movies = movieService.findMoviesByTitleContaining(title, pageable);

            return new ResponseEntity<>(movies, HttpStatus.OK);

        } catch (IllegalArgumentException e) {

            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        }

    }

  

    /**

     * 根据年份范围获取电影

     */

    @GetMapping("/by-year-range")

    public ResponseEntity<List<Movie>> getMoviesByYearRange(

            @RequestParam Integer start,

            @RequestParam Integer end) {

        try {

            List<Movie> movies = movieService.findMoviesByReleasedBetween(start, end);

            return new ResponseEntity<>(movies, HttpStatus.OK);

        } catch (IllegalArgumentException e) {

            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        }

    }

  

    /**

     * 获取电影的演员

     */

    @GetMapping("/{id}/actors")

    public ResponseEntity<List<Person>> getActorsInMovie(@PathVariable Long id) {

        try {

            Optional<Movie> movieOpt = movieService.findMovieById(id);

            if (!movieOpt.isPresent()) {

                return new ResponseEntity<>(HttpStatus.NOT_FOUND);

            }

  

            List<Person> actors = personService.findActorsInMovie(movieOpt.get().getTitle());

            return new ResponseEntity<>(actors, HttpStatus.OK);

        } catch (IllegalArgumentException e) {

            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        }

    }

  

    /**

     * 创建电影并添加演员和导演

     */

    @PostMapping("/create-with-cast")

    public ResponseEntity<Movie> createMovieWithCast(@RequestBody Map<String, Object> request) {

        try {

            // 从请求体中提取电影信息

            Object movieDataObj = request.get("movie");

            if (movieDataObj == null) {

                return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

            }

  

            Map<String, Object> movieData = (Map<String, Object>) movieDataObj;

            Movie movie = new Movie();

  

            if (movieData.containsKey("title")) {

                movie.setTitle((String) movieData.get("title"));

            }

  

            if (movieData.containsKey("released")) {

                Integer released = (Integer) movieData.get("released");

                movie.setReleased(released);

            }

  

            // 提取演员姓名列表

            @SuppressWarnings("unchecked")

            List<String> actorNames = (List<String>) request.get("actorNames");

  

            // 提取导演姓名列表

            @SuppressWarnings("unchecked")

            List<String> directorNames = (List<String>) request.get("directorNames");

  

            // 创建电影并添加演员和导演

            Movie savedMovie = movieService.createMovieWithActorsAndDirectors(movie, actorNames, directorNames);

  

            return new ResponseEntity<>(savedMovie, HttpStatus.CREATED);

        } catch (ClassCastException | NullPointerException e) {

            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        } catch (IllegalArgumentException e) {

            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

        } catch (Exception e) {

            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);

        }

    }

}

Spring Boot 主类

创建 Spring Boot 应用程序的主类:


package com.example.graphmovieapp;

  

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

  

/**

 * Spring Boot应用程序主类

 * Neo4j电影图数据库应用程序的入口点

 */

@SpringBootApplication

public class GraphMovieAppApplication {

  

    public static void main(String[] args) {

        SpringApplication.run(GraphMovieAppApplication.class, args);

    }

}

高级特性

1. 分页查询

Spring Data Neo4j 支持分页查询,便于处理大量数据:


// 在Repository中定义分页查询方法

@Query(value = "MATCH (m:Movie) WHERE m.title CONTAINS $title RETURN m ORDER BY m.title SKIP $skip LIMIT $limit",

       countQuery = "MATCH (m:Movie) WHERE m.title CONTAINS $title RETURN count(m)")

Page<Movie> findByTitleContaining(@Param("title") String title, Pageable pageable);

  

// 在Service中使用分页

@Transactional(readOnly = true)

public Page<Movie> findMoviesByTitleContaining(String title, Pageable pageable) {

    if (title == null || title.trim().isEmpty()) {

        throw new IllegalArgumentException("标题关键字不能为空");

    }

    if (pageable == null) {

        throw new IllegalArgumentException("分页参数不能为空");

    }

    return movieRepository.findByTitleContaining(title, pageable);

}

  

// 在Controller中提供分页端点

@GetMapping("/search")

public ResponseEntity<Page<Movie>> searchMoviesByTitle(

        @RequestParam String title,

        @RequestParam(defaultValue = "0") int page,

        @RequestParam(defaultValue = "10") int size) {

    try {

        Pageable pageable = PageRequest.of(page, size);

        Page<Movie> movies = movieService.findMoviesByTitleContaining(title, pageable);

        return new ResponseEntity<>(movies, HttpStatus.OK);

    } catch (IllegalArgumentException e) {

        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

    }

}

2. 复杂Cypher查询

支持执行复杂的图数据库查询:


// 查找合作演员

@Query("MATCH (p1:Person {name: $name})-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) WHERE p1 <> p2 RETURN DISTINCT p2")

List<Person> findCoActors(@Param("name") String name);

  

// 查找同时担任演员和导演的电影

@Query("MATCH (p:Person {name: $name})-[:ACTED_IN]->(m:Movie) WHERE (p)-[:DIRECTED]->(m) RETURN m")

List<Movie> findMoviesActedAndDirectedByPerson(@Param("name") String name);

  

// 统计演员参演的电影数量

@Query("MATCH (p:Person {name: $name})-[:ACTED_IN]->(m:Movie) RETURN count(m)")

Long countMoviesActedByPerson(@Param("name") String name);

3. JSON序列化控制

使用FastJSON注解控制关系的序列化:


@Relationship(type = "ACTED_IN", direction = Relationship.Direction.OUTGOING)

@JSONField(serialize = false, deserialize = false)

private List<Movie> actedInMovies = new ArrayList<>();

这可以避免在JSON序列化时出现循环引用问题。

4. 事务管理

使用Spring的@Transactional注解进行事务管理:


@Service

@Transactional

public class MovieServiceImpl implements MovieService {

  

    @Override

    @Transactional(readOnly = true)

    public List<Movie> findAllMovies() {

        return movieRepository.findAll();

    }

  

    @Override

    @Transactional

    public Movie createMovieWithActorsAndDirectors(Movie movie, List<String> actorNames, List<String> directorNames) {

        // 事务性的复杂操作

        // ...

    }

}

5. 验证注解

使用Jakarta验证注解进行数据验证:


@Property("name")

@NotBlank(message = "人物姓名不能为空")

private String name;

  

@Property("born")

@Min(value = 1800, message = "出生年份不能早于1800年")

@Max(value = 2100, message = "出生年份不能晚于2100年")

private Integer born;

运行应用

创建完所有组件后,可以运行应用程序:

1. 启动应用程序


mvn spring-boot:run

应用程序将在默认端口8080上启动。

2. 测试API端点

可以使用curl或其他HTTP客户端测试API:

创建电影


curl -X POST http://localhost:8080/api/movies \

  -H "Content-Type: application/json" \

  -d '{"title": "The Matrix", "released": 1999}'

创建带演员和导演的电影


curl -X POST http://localhost:8080/api/movies/create-with-cast \

  -H "Content-Type: application/json" \

  -d '{

    "movie": {

      "title": "Inception",

      "released": 2010

    },

    "actorNames": ["Leonardo DiCaprio", "Tom Hardy"],

    "directorNames": ["Christopher Nolan"]

  }'

搜索电影


curl "http://localhost:8080/api/movies/search?title=Matrix&page=0&size=10"

根据年份范围查询电影


curl "http://localhost:8080/api/movies/by-year-range?start=1990&end=2000"

获取电影的演员


curl http://localhost:8080/api/movies/1/actors

3. 测试用例

项目包含了完整的测试套件,包括:

  1. 单元测试:测试Repository、Service和Controller层

  2. 集成测试:使用TestContainers测试整个应用程序

  3. 端到端测试:测试完整的API工作流程

运行测试:


mvn test

总结

通过本教程,我们构建了一个完整的基于 Spring Data Neo4j 的电影图数据库应用程序。该项目展示了:

核心功能

  1. 图数据库建模

- 使用 @Node 注解定义人物和电影节点

- 使用 @Relationship 注解定义演员和导演关系

- 支持双向关系导航

  1. 丰富的数据访问

- 基本CRUD操作

- 复杂的图查询(合作演员、多角色人物等)

- 分页查询支持

- 条件查询和统计查询

  1. 分层架构设计

- Repository层:数据访问抽象

- Service层:业务逻辑处理

- Controller层:REST API接口

- 清晰的职责分离

  1. 高级特性

- 事务管理

- 数据验证

- JSON序列化控制

- 异常处理

  1. 测试覆盖

- 单元测试、集成测试

- 使用TestContainers进行数据库测试

- TDD开发方法

项目特点

  • 完整的电影图数据库应用:包含人物、电影、演员关系、导演关系等完整功能

  • 现代化的技术栈:Spring Boot 3.2.0、Neo4j 5.x、Java 17

  • 企业级代码质量:完整的注解、异常处理、事务管理

  • RESTful API设计:提供完整的HTTP接口

  • 全面的测试策略:确保代码质量和可靠性

Spring Data Neo4j 提供了强大的功能来操作 Neo4j 图数据库,使得开发者可以:

  • 使用注解轻松定义节点和关系

  • 通过仓库接口简化数据访问

  • 使用自定义 Cypher 查询处理复杂查询需求

  • 利用 Spring 的事务管理确保数据一致性

  • 支持分页、排序等高级功能

通过这个实际项目,你应该能够开始使用 Spring Data Neo4j 构建自己的图数据库应用程序,处理复杂的关系数据模型。

posted @ 2025-10-06 01:00  suveng  阅读(141)  评论(0)    收藏  举报