Spring Data Neo4j 简单教程
简介
Spring Data Neo4j 是 Spring Data 项目的一部分,它提供了对 Neo4j 图数据库的集成支持。通过 Spring Data Neo4j,开发者可以轻松地在 Spring Boot 应用中使用 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插件包含:
- 数据处理功能:JSON处理、XML处理、日期时间操作等
- 图算法:最短路径、中心性算法、社区检测等
- 数据导入导出:从各种数据源(CSV、JSON、XML、数据库等)导入数据
- 图重构:节点合并、关系合并、图变换等
- 元数据操作:索引管理、约束管理、模式信息查询等
- 虚拟关系和节点:动态创建不实际存储在数据库中的关系和节点
APOC插件是可选的,但对于生产环境和复杂应用场景非常有用。如果您不需要这些额外功能,可以移除 NEO4J_PLUGINS=["apoc"]
环境变量。
- 官方下载安装:访问 Neo4j 官网 下载对应平台的安装包
2. 创建 Spring Boot 项目
创建一个新的 Spring Boot 项目,推荐使用 Spring Boot 3.x 版本(如 3.2.0)以确保与 Neo4j 5.x 的兼容性。
在 pom.xml
中添加以下依赖:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version><relativePath/>
</parent><dependencies><!-- Neo4j 数据库支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-neo4j</artifactId></dependency><!-- Web 支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- JUnit 5 (通常已包含在 spring-boot-starter-test 中) --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><scope>test</scope></dependency><!-- Mockito (通常已包含在 spring-boot-starter-test 中) --><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><scope>test</scope></dependency><!-- 测试容器 - 用于集成测试中的 Neo4j 实例 --><dependency><groupId>org.testcontainers</groupId><artifactId>junit-jupiter</artifactId><scope>test</scope></dependency><dependency><groupId>org.testcontainers</groupId><artifactId>neo4j</artifactId><scope>test</scope></dependency>
</dependencies>
测试依赖说明
-
spring-boot-starter-test:
- Spring Boot 的核心测试启动器
- 包含了大多数测试所需的库,如 JUnit 5、Mockito、AssertJ 等
- 提供了 Spring Boot 测试上下文支持
-
junit-jupiter:
- JUnit 5 的核心模块
- 提供了现代的测试框架和注解(@Test、@BeforeEach、@AfterEach 等)
- 支持参数化测试和动态测试
-
mockito-core:
- 强大的模拟框架
- 用于创建和管理模拟对象
- 支持 BDD(行为驱动开发)风格的测试
-
testcontainers:
- 提供轻量级的、一次性的数据库实例
- 用于集成测试,确保测试环境的一致性
- 支持多种数据库,包括 Neo4j
-
testcontainers-neo4j:
- TestContainers 的 Neo4j 模块
- 专门用于在测试中启动和管理 Neo4j 容器
- 确保每个测试都有干净的数据库环境
测试配置
在 src/test/resources/application-test.yml
中添加测试配置:
spring:neo4j:uri: bolt://localhost:7687authentication:username: neo4jpassword: test_passwordlogging: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 {@Containerstatic Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5.15.0-community").withAdminPassword("test_password");@DynamicPropertySourcestatic 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.properties
或 application.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:7687authentication:username: neo4jpassword: your_password
定义节点实体
使用 @Node
注解定义图数据库中的节点实体:
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;@Node("Person")
public class Person {@Idprivate Long id;@Property("name")private String name;@Property("born")private Integer born;// 构造函数、getter和setter方法public Person() {}public Person(String name, Integer born) {this.name = name;this.born = born;}// 省略getter和setter方法...
}
定义关系
使用 @Relationship
注解定义节点之间的关系:
@Node("Person")
public class Person {// ... 其他属性@Relationship(type = "ACTED_IN", direction = Relationship.Direction.OUTGOING)private List<Movie> actedInMovies = new ArrayList<>();@Relationship(type = "DIRECTED", direction = Relationship.Direction.OUTGOING)private List<Movie> directedMovies = new ArrayList<>();// ... 其他代码
}@Node("Movie")
public class Movie {@Idprivate Long id;@Property("title")private String title;@Property("released")private Integer released;@Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)private List<Person> actors = new ArrayList<>();@Relationship(type = "DIRECTED", direction = Relationship.Direction.INCOMING)private List<Person> directors = new ArrayList<>();// 构造函数、getter和setter方法public Movie() {}public Movie(String title, Integer released) {this.title = title;this.released = released;}// 省略getter和setter方法...
}
创建仓库接口
创建继承自 Neo4jRepository
的仓库接口:
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.stereotype.Repository;@Repository
public interface PersonRepository extends Neo4jRepository<Person, Long> {// 根据名称查找人物Person findByName(String name);// 根据出生年份查找人物List<Person> findByBorn(Integer year);// 自定义查询:查找参演过特定电影的人物@Query("MATCH (p:Person)-[:ACTED_IN]->(m:Movie) WHERE m.title = $title RETURN p")List<Person> findActorsInMovie(String title);
}@Repository
public interface MovieRepository extends Neo4jRepository<Movie, Long> {// 根据标题查找电影Movie findByTitle(String title);// 根据发行年份查找电影List<Movie> findByReleased(Integer year);// 自定义查询:查找特定导演执导的电影@Query("MATCH (p:Person)-[:DIRECTED]->(m:Movie) WHERE p.name = $directorName RETURN m")List<Movie> findMoviesByDirector(String directorName);
}
创建服务层
创建服务层来处理业务逻辑:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class MovieService {private final PersonRepository personRepository;private final MovieRepository movieRepository;@Autowiredpublic MovieService(PersonRepository personRepository, MovieRepository movieRepository) {this.personRepository = personRepository;this.movieRepository = movieRepository;}// 添加人物public Person addPerson(Person person) {return personRepository.save(person);}// 添加电影public Movie addMovie(Movie movie) {return movieRepository.save(movie);}// 查找所有人物public List<Person> findAllPersons() {return personRepository.findAll();}// 查找所有电影public List<Movie> findAllMovies() {return movieRepository.findAll();}// 建立演员与电影的关系public Person addActorToMovie(String personName, String movieTitle) {Person person = personRepository.findByName(personName);Movie movie = movieRepository.findByTitle(movieTitle);if (person != null && movie != null) {person.getActedInMovies().add(movie);return personRepository.save(person);}return null;}// 建立导演与电影的关系public Person addDirectorToMovie(String personName, String movieTitle) {Person person = personRepository.findByName(personName);Movie movie = movieRepository.findByTitle(movieTitle);if (person != null && movie != null) {person.getDirectedMovies().add(movie);return personRepository.save(person);}return null;}// 查找参演特定电影的演员public List<Person> findActorsInMovie(String movieTitle) {return personRepository.findActorsInMovie(movieTitle);}// 查找特定导演执导的电影public List<Movie> findMoviesByDirector(String directorName) {return movieRepository.findMoviesByDirector(directorName);}
}
创建控制器
创建 REST API 控制器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.util.List;@RestController
@RequestMapping("/api")
public class MovieController {private final MovieService movieService;@Autowiredpublic MovieController(MovieService movieService) {this.movieService = movieService;}// 添加人物@PostMapping("/persons")public ResponseEntity<Person> addPerson(@RequestBody Person person) {Person savedPerson = movieService.addPerson(person);return ResponseEntity.ok(savedPerson);}// 添加电影@PostMapping("/movies")public ResponseEntity<Movie> addMovie(@RequestBody Movie movie) {Movie savedMovie = movieService.addMovie(movie);return ResponseEntity.ok(savedMovie);}// 获取所有人物@GetMapping("/persons")public ResponseEntity<List<Person>> getAllPersons() {List<Person> persons = movieService.findAllPersons();return ResponseEntity.ok(persons);}// 获取所有电影@GetMapping("/movies")public ResponseEntity<List<Movie>> getAllMovies() {List<Movie> movies = movieService.findAllMovies();return ResponseEntity.ok(movies);}// 添加演员到电影@PostMapping("/movies/{movieTitle}/actors")public ResponseEntity<Person> addActorToMovie(@PathVariable String movieTitle, @RequestParam String personName) {Person person = movieService.addActorToMovie(personName, movieTitle);if (person != null) {return ResponseEntity.ok(person);}return ResponseEntity.notFound().build();}// 添加导演到电影@PostMapping("/movies/{movieTitle}/directors")public ResponseEntity<Person> addDirectorToMovie(@PathVariable String movieTitle, @RequestParam String personName) {Person person = movieService.addDirectorToMovie(personName, movieTitle);if (person != null) {return ResponseEntity.ok(person);}return ResponseEntity.notFound().build();}// 获取电影中的演员@GetMapping("/movies/{movieTitle}/actors")public ResponseEntity<List<Person>> getActorsInMovie(@PathVariable String movieTitle) {List<Person> actors = movieService.findActorsInMovie(movieTitle);return ResponseEntity.ok(actors);}// 获取导演的电影@GetMapping("/directors/{directorName}/movies")public ResponseEntity<List<Movie>> getMoviesByDirector(@PathVariable String directorName) {List<Movie> movies = movieService.findMoviesByDirector(directorName);return ResponseEntity.ok(movies);}
}
自定义查询
Spring Data Neo4j 支持使用 @Query
注解执行自定义 Cypher 查询:
@Repository
public interface MovieRepository extends Neo4jRepository<Movie, Long> {// 查找演员数量超过指定数量的电影@Query("MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person) " +"WITH m, count(p) as actorCount " +"WHERE actorCount > $minActors " +"RETURN m")List<Movie> findMoviesWithMoreThanActors(@Param("minActors") Integer minActors);// 查找既是演员又是导演的人物@Query("MATCH (p:Person)-[:ACTED_IN]->(:Movie), " +"(p:Person)-[:DIRECTED]->(:Movie) " +"RETURN DISTINCT p")List<Person> findActorsWhoAreAlsoDirectors();// 查找演员合作过的其他演员@Query("MATCH (p1:Person {name: $actorName})-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) " +"WHERE p1 <> p2 " +"RETURN DISTINCT p2")List<Person> findCoActors(@Param("actorName") String actorName);
}
事务管理
Spring Data Neo4j 支持 Spring 的事务管理。可以使用 @Transactional
注解来管理事务:
import org.springframework.transaction.annotation.Transactional;@Service
public class MovieService {// ... 其他代码@Transactionalpublic void createMovieWithCast(Movie movie, List<Person> actors, Person director) {// 保存电影Movie savedMovie = movieRepository.save(movie);// 保存演员List<Person> savedActors = new ArrayList<>();for (Person actor : actors) {Person savedActor = personRepository.save(actor);savedActor.getActedInMovies().add(savedMovie);savedActors.add(personRepository.save(savedActor));}// 保存导演Person savedDirector = personRepository.save(director);savedDirector.getDirectedMovies().add(savedMovie);personRepository.save(savedDirector);}
}
测试应用
使用TDD(测试驱动开发)思想,我们需要为应用的所有核心功能编写全面的测试。以下是完整的测试用例:
1. 仓库层测试
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import org.springframework.test.context.ActiveProfiles;import java.util.Arrays;
import java.util.List;
import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;@DataNeo4jTest
@ActiveProfiles("test")
class PersonRepositoryTest {@Autowiredprivate PersonRepository personRepository;@Autowiredprivate MovieRepository movieRepository;private Person tomHanks;private Movie forrestGump;@BeforeEachvoid setUp() {personRepository.deleteAll();movieRepository.deleteAll();tomHanks = new Person("Tom Hanks", 1956);forrestGump = new Movie("Forrest Gump", 1994);personRepository.save(tomHanks);movieRepository.save(forrestGump);}@Testvoid testFindByName() {Optional<Person> foundPerson = Optional.ofNullable(personRepository.findByName("Tom Hanks"));assertTrue(foundPerson.isPresent());assertEquals("Tom Hanks", foundPerson.get().getName());assertEquals(1956, foundPerson.get().getBorn());}@Testvoid testFindByBorn() {List<Person> people = personRepository.findByBorn(1956);assertEquals(1, people.size());assertEquals("Tom Hanks", people.get(0).getName());}@Testvoid testFindActorsInMovie() {// 建立关系tomHanks.getActedInMovies().add(forrestGump);personRepository.save(tomHanks);List<Person> actors = personRepository.findActorsInMovie("Forrest Gump");assertEquals(1, actors.size());assertEquals("Tom Hanks", actors.get(0).getName());}
}@DataNeo4jTest
@ActiveProfiles("test")
class MovieRepositoryTest {@Autowiredprivate MovieRepository movieRepository;@Autowiredprivate PersonRepository personRepository;private Movie forrestGump;private Person tomHanks;private Person robertZemeckis;@BeforeEachvoid setUp() {movieRepository.deleteAll();personRepository.deleteAll();forrestGump = new Movie("Forrest Gump", 1994);tomHanks = new Person("Tom Hanks", 1956);robertZemeckis = new Person("Robert Zemeckis", 1952);movieRepository.save(forrestGump);personRepository.save(tomHanks);personRepository.save(robertZemeckis);}@Testvoid testFindByTitle() {Optional<Movie> foundMovie = Optional.ofNullable(movieRepository.findByTitle("Forrest Gump"));assertTrue(foundMovie.isPresent());assertEquals("Forrest Gump", foundMovie.get().getTitle());assertEquals(1994, foundMovie.get().getReleased());}@Testvoid testFindByReleased() {List<Movie> movies = movieRepository.findByReleased(1994);assertEquals(1, movies.size());assertEquals("Forrest Gump", movies.get(0).getTitle());}@Testvoid testFindMoviesByDirector() {// 建立关系robertZemeckis.getDirectedMovies().add(forrestGump);personRepository.save(robertZemeckis);List<Movie> movies = movieRepository.findMoviesByDirector("Robert Zemeckis");assertEquals(1, movies.size());assertEquals("Forrest Gump", movies.get(0).getTitle());}@Testvoid testFindMoviesWithMoreThanActors() {// 添加更多演员Person actor1 = new Person("Actor 1", 1980);Person actor2 = new Person("Actor 2", 1980);personRepository.saveAll(Arrays.asList(actor1, actor2));// 建立关系forrestGump.getActors().addAll(Arrays.asList(tomHanks, actor1, actor2));movieRepository.save(forrestGump);List<Movie> movies = movieRepository.findMoviesWithMoreThanActors(2);assertEquals(1, movies.size());assertEquals("Forrest Gump", movies.get(0).getTitle());}@Testvoid testFindActorsWhoAreAlsoDirectors() {// 创建一个既是演员又是导演的人Person actorDirector = new Person("Actor Director", 1970);personRepository.save(actorDirector);// 创建两部电影Movie movie1 = new Movie("Movie 1", 2000);Movie movie2 = new Movie("Movie 2", 2001);movieRepository.saveAll(Arrays.asList(movie1, movie2));// 建立关系actorDirector.getActedInMovies().add(movie1);actorDirector.getDirectedMovies().add(movie2);personRepository.save(actorDirector);List<Person> people = personRepository.findActorsWhoAreAlsoDirectors();assertEquals(1, people.size());assertEquals("Actor Director", people.get(0).getName());}
}
2. 服务层测试
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;import java.util.Arrays;
import java.util.List;
import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;@ExtendWith(MockitoExtension.class)
class MovieServiceTest {@Mockprivate PersonRepository personRepository;@Mockprivate MovieRepository movieRepository;@InjectMocksprivate MovieService movieService;private Person tomHanks;private Movie forrestGump;@BeforeEachvoid setUp() {tomHanks = new Person("Tom Hanks", 1956);forrestGump = new Movie("Forrest Gump", 1994);}@Testvoid testAddPerson() {when(personRepository.save(any(Person.class))).thenReturn(tomHanks);Person savedPerson = movieService.addPerson(tomHanks);assertNotNull(savedPerson);assertEquals("Tom Hanks", savedPerson.getName());verify(personRepository, times(1)).save(tomHanks);}@Testvoid testAddMovie() {when(movieRepository.save(any(Movie.class))).thenReturn(forrestGump);Movie savedMovie = movieService.addMovie(forrestGump);assertNotNull(savedMovie);assertEquals("Forrest Gump", savedMovie.getTitle());verify(movieRepository, times(1)).save(forrestGump);}@Testvoid testFindAllPersons() {List<Person> persons = Arrays.asList(tomHanks);when(personRepository.findAll()).thenReturn(persons);List<Person> result = movieService.findAllPersons();assertEquals(1, result.size());assertEquals("Tom Hanks", result.get(0).getName());verify(personRepository, times(1)).findAll();}@Testvoid testFindAllMovies() {List<Movie> movies = Arrays.asList(forrestGump);when(movieRepository.findAll()).thenReturn(movies);List<Movie> result = movieService.findAllMovies();assertEquals(1, result.size());assertEquals("Forrest Gump", result.get(0).getTitle());verify(movieRepository, times(1)).findAll();}@Testvoid testAddActorToMovie_Success() {when(personRepository.findByName("Tom Hanks")).thenReturn(tomHanks);when(movieRepository.findByTitle("Forrest Gump")).thenReturn(forrestGump);when(personRepository.save(any(Person.class))).thenReturn(tomHanks);Person result = movieService.addActorToMovie("Tom Hanks", "Forrest Gump");assertNotNull(result);assertEquals(1, result.getActedInMovies().size());assertEquals("Forrest Gump", result.getActedInMovies().get(0).getTitle());verify(personRepository, times(1)).save(tomHanks);}@Testvoid testAddActorToMovie_PersonNotFound() {when(personRepository.findByName("Tom Hanks")).thenReturn(null);when(movieRepository.findByTitle("Forrest Gump")).thenReturn(forrestGump);Person result = movieService.addActorToMovie("Tom Hanks", "Forrest Gump");assertNull(result);verify(personRepository, never()).save(any(Person.class));}@Testvoid testAddActorToMovie_MovieNotFound() {when(personRepository.findByName("Tom Hanks")).thenReturn(tomHanks);when(movieRepository.findByTitle("Forrest Gump")).thenReturn(null);Person result = movieService.addActorToMovie("Tom Hanks", "Forrest Gump");assertNull(result);verify(personRepository, never()).save(any(Person.class));}@Testvoid testAddDirectorToMovie_Success() {when(personRepository.findByName("Robert Zemeckis")).thenReturn(tomHanks);when(movieRepository.findByTitle("Forrest Gump")).thenReturn(forrestGump);when(personRepository.save(any(Person.class))).thenReturn(tomHanks);Person result = movieService.addDirectorToMovie("Robert Zemeckis", "Forrest Gump");assertNotNull(result);assertEquals(1, result.getDirectedMovies().size());assertEquals("Forrest Gump", result.getDirectedMovies().get(0).getTitle());verify(personRepository, times(1)).save(tomHanks);}@Testvoid testFindActorsInMovie() {List<Person> actors = Arrays.asList(tomHanks);when(personRepository.findActorsInMovie("Forrest Gump")).thenReturn(actors);List<Person> result = movieService.findActorsInMovie("Forrest Gump");assertEquals(1, result.size());assertEquals("Tom Hanks", result.get(0).getName());verify(personRepository, times(1)).findActorsInMovie("Forrest Gump");}@Testvoid testFindMoviesByDirector() {List<Movie> movies = Arrays.asList(forrestGump);when(movieRepository.findMoviesByDirector("Robert Zemeckis")).thenReturn(movies);List<Movie> result = movieService.findMoviesByDirector("Robert Zemeckis");assertEquals(1, result.size());assertEquals("Forrest Gump", result.get(0).getTitle());verify(movieRepository, times(1)).findMoviesByDirector("Robert Zemeckis");}@Testvoid testCreateMovieWithCast() {Person director = new Person("Robert Zemeckis", 1952);List<Person> actors = Arrays.asList(tomHanks);when(movieRepository.save(any(Movie.class))).thenReturn(forrestGump);when(personRepository.save(any(Person.class))).thenReturn(tomHanks).thenReturn(director);movieService.createMovieWithCast(forrestGump, actors, director);verify(movieRepository, times(1)).save(forrestGump);verify(personRepository, times(3)).save(any(Person.class)); // 2 actors + 1 director}
}
3. 控制器层测试
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;import java.util.Arrays;
import java.util.List;import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@WebMvcTest(MovieController.class)
class MovieControllerTest {@Autowiredprivate MockMvc mockMvc;@MockBeanprivate MovieService movieService;@Autowiredprivate ObjectMapper objectMapper;private Person tomHanks;private Movie forrestGump;@BeforeEachvoid setUp() {tomHanks = new Person("Tom Hanks", 1956);tomHanks.setId(1L);forrestGump = new Movie("Forrest Gump", 1994);forrestGump.setId(1L);}@Testvoid testAddPerson() throws Exception {when(movieService.addPerson(any(Person.class))).thenReturn(tomHanks);mockMvc.perform(post("/api/persons").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(tomHanks))).andExpect(status().isOk()).andExpect(jsonPath("$.name").value("Tom Hanks")).andExpect(jsonPath("$.born").value(1956));}@Testvoid testAddMovie() throws Exception {when(movieService.addMovie(any(Movie.class))).thenReturn(forrestGump);mockMvc.perform(post("/api/movies").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(forrestGump))).andExpect(status().isOk()).andExpect(jsonPath("$.title").value("Forrest Gump")).andExpect(jsonPath("$.released").value(1994));}@Testvoid testGetAllPersons() throws Exception {List<Person> persons = Arrays.asList(tomHanks);when(movieService.findAllPersons()).thenReturn(persons);mockMvc.perform(get("/api/persons")).andExpect(status().isOk()).andExpect(jsonPath("$[0].name").value("Tom Hanks")).andExpect(jsonPath("$[0].born").value(1956));}@Testvoid testGetAllMovies() throws Exception {List<Movie> movies = Arrays.asList(forrestGump);when(movieService.findAllMovies()).thenReturn(movies);mockMvc.perform(get("/api/movies")).andExpect(status().isOk()).andExpect(jsonPath("$[0].title").value("Forrest Gump")).andExpect(jsonPath("$[0].released").value(1994));}@Testvoid testAddActorToMovie() throws Exception {when(movieService.addActorToMovie("Tom Hanks", "Forrest Gump")).thenReturn(tomHanks);mockMvc.perform(post("/api/movies/Forrest Gump/actors").param("personName", "Tom Hanks")).andExpect(status().isOk()).andExpect(jsonPath("$.name").value("Tom Hanks"));}@Testvoid testAddActorToMovie_NotFound() throws Exception {when(movieService.addActorToMovie("Tom Hanks", "Forrest Gump")).thenReturn(null);mockMvc.perform(post("/api/movies/Forrest Gump/actors").param("personName", "Tom Hanks")).andExpect(status().isNotFound());}@Testvoid testAddDirectorToMovie() throws Exception {when(movieService.addDirectorToMovie("Robert Zemeckis", "Forrest Gump")).thenReturn(tomHanks);mockMvc.perform(post("/api/movies/Forrest Gump/directors").param("personName", "Robert Zemeckis")).andExpect(status().isOk()).andExpect(jsonPath("$.name").value("Tom Hanks"));}@Testvoid testGetActorsInMovie() throws Exception {List<Person> actors = Arrays.asList(tomHanks);when(movieService.findActorsInMovie("Forrest Gump")).thenReturn(actors);mockMvc.perform(get("/api/movies/Forrest Gump/actors")).andExpect(status().isOk()).andExpect(jsonPath("$[0].name").value("Tom Hanks"));}@Testvoid testGetMoviesByDirector() throws Exception {List<Movie> movies = Arrays.asList(forrestGump);when(movieService.findMoviesByDirector("Robert Zemeckis")).thenReturn(movies);mockMvc.perform(get("/api/directors/Robert Zemeckis/movies")).andExpect(status().isOk()).andExpect(jsonPath("$[0].title").value("Forrest Gump"));}
}
4. 集成测试
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;import java.util.Arrays;
import java.util.List;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class MovieIntegrationTest {@LocalServerPortprivate int port;@Autowiredprivate TestRestTemplate restTemplate;@Autowiredprivate PersonRepository personRepository;@Autowiredprivate MovieRepository movieRepository;@BeforeEachvoid setUp() {personRepository.deleteAll();movieRepository.deleteAll();}@Testvoid testFullWorkflow() {// 1. 添加人物Person person = new Person("Tom Hanks", 1956);ResponseEntity<Person> personResponse = restTemplate.postForEntity("http://localhost:" + port + "/api/persons", person, Person.class);assertEquals(200, personResponse.getStatusCodeValue());assertNotNull(personResponse.getBody().getId());// 2. 添加电影Movie movie = new Movie("Forrest Gump", 1994);ResponseEntity<Movie> movieResponse = restTemplate.postForEntity("http://localhost:" + port + "/api/movies", movie, Movie.class);assertEquals(200, movieResponse.getStatusCodeValue());assertNotNull(movieResponse.getBody().getId());// 3. 添加演员到电影ResponseEntity<Person> actorResponse = restTemplate.postForEntity("http://localhost:" + port + "/api/movies/Forrest Gump/actors?personName=Tom Hanks",null, Person.class);assertEquals(200, actorResponse.getStatusCodeValue());// 4. 查询电影中的演员ResponseEntity<Person[]> actorsResponse = restTemplate.getForEntity("http://localhost:" + port + "/api/movies/Forrest Gump/actors",Person[].class);assertEquals(200, actorsResponse.getStatusCodeValue());List<Person> actors = Arrays.asList(actorsResponse.getBody());assertEquals(1, actors.size());assertEquals("Tom Hanks", actors.get(0).getName());}
}
5. 测试配置
在 src/test/resources/application-test.yml
中添加测试配置:
spring:neo4j:uri: bolt://localhost:7687authentication:username: neo4jpassword: test_passwordlogging:level:org.springframework.data.neo4j: DEBUG
测试覆盖的功能点
通过以上测试,我们覆盖了以下功能点:
-
仓库层:
- 基本CRUD操作
- 自定义查询方法
- 关系查询
-
服务层:
- 业务逻辑验证
- 异常处理
- 事务管理
-
控制器层:
- REST API端点
- 请求/响应序列化
- HTTP状态码
-
集成测试:
- 完整工作流程
- 组件间交互
这种全面的测试策略确保了应用的各个层次和功能都得到了验证,提高了代码质量和可靠性。
运行应用
创建 Spring Boot 主类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class Neo4jApplication {public static void main(String[] args) {SpringApplication.run(Neo4jApplication.class, args);}
}
运行应用后,可以通过以下方式测试 API:
- 添加人物:
curl -X POST http://localhost:8080/api/persons \-H "Content-Type: application/json" \-d '{"name": "Tom Hanks", "born": 1956}'
- 添加电影:
curl -X POST http://localhost:8080/api/movies \-H "Content-Type: application/json" \-d '{"title": "Forrest Gump", "released": 1994}'
- 添加演员到电影:
curl -X POST http://localhost:8080/api/movies/Forrest%20Gump/actors?personName=Tom%20Hanks
- 查询电影中的演员:
curl http://localhost:8080/api/movies/Forrest%20Gump/actors
高级特性
1. 自定义 ID 生成
可以使用 @Id
注解结合 @GeneratedValue
来自定义 ID 生成策略:
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.GeneratedValue.UUIDGenerator;@Node("Person")
public class Person {@Id@GeneratedValue(UUIDGenerator.class)private String id;// ... 其他属性
}
2. 条件查询
使用 @Query
注解进行条件查询:
@Repository
public interface MovieRepository extends Neo4jRepository<Movie, Long> {// 查找指定年份范围内发行的电影@Query("MATCH (m:Movie) WHERE m.released >= $startYear AND m.released <= $endYear RETURN m")List<Movie> findMoviesByYearRange(@Param("startYear") Integer startYear, @Param("endYear") Integer endYear);
}
3. 分页和排序
Spring Data Neo4j 支持分页和排序:
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;@Repository
public interface MovieRepository extends Neo4jRepository<Movie, Long> {// 分页查询所有电影Page<Movie> findAll(Pageable pageable);// 按发行年份排序查询电影List<Movie> findByReleased(Integer year, Sort sort);
}
在服务层使用:
@Service
public class MovieService {// ... 其他代码public Page<Movie> findMoviesWithPagination(int page, int size) {Pageable pageable = PageRequest.of(page, size);return movieRepository.findAll(pageable);}public List<Movie> findMoviesByYearSorted(Integer year) {Sort sort = Sort.by("title").ascending();return movieRepository.findByReleased(year, sort);}
}
总结
Spring Data Neo4j 提供了强大的功能来操作 Neo4j 图数据库,使得开发者可以:
- 使用注解轻松定义节点和关系
- 通过仓库接口简化数据访问
- 使用自定义 Cypher 查询处理复杂查询需求
- 利用 Spring 的事务管理确保数据一致性
- 支持分页、排序等高级功能
通过本教程,你应该能够开始使用 Spring Data Neo4j 构建基于图数据库的应用程序,处理复杂的关系数据模型。