在当今大数据时代,传统的关系型数据库在处理复杂关系网络时往往力不从心。Neo4j 作为领先的图数据库,能够高效地存储和查询海量关系数据。本文将详细介绍如何在 Spring Boot 项目中集成 Neo4j,并提供完整的实战案例,帮助读者快速掌握图数据库的开发技巧。
一、图数据库概述与 Neo4j 简介
1.1 为什么选择图数据库
在传统的关系型数据库中,当我们需要查询"朋友的朋友"这样的多跳关系时,往往需要编写复杂的多表关联查询,性能随关系层数增加呈指数级下降。而图数据库天然适合处理这类场景,它将数据之间的关系作为核心Citizens,利用图遍历算法高效地查询关系网络。
图数据库的核心优势体现在以下几个方面。首先是性能优势,对于深度关系查询,图数据库的性能是关系型数据库的数倍甚至数十倍。其次是灵活性优势,图数据库的 schema 更加灵活,可以随时添加新的节点类型和关系类型,而无需修改表结构。第三是表达力优势,图的数据模型更加直观,使用节点和边来描述现实世界的关系,与人类的思维方式更加契合。
Neo4j 是目前最流行的图数据库之一,它使用 Cypher 查询语言,具有高性能、高可用、易于使用等特点。Neo4j 的核心概念包括节点(Node)、关系(Relationship)和属性(Property)。节点代表实体,关系连接节点,属性则存储节点和关系的详细信息。
1.2 Spring Data Neo4j 概述
Spring Data Neo4j 是 Spring Data 项目的一部分,它为 Neo4j 提供了便捷的集成方案。通过 Spring Data Neo4j,开发者可以使用声明式的方式操作图数据,无需编写繁琐的底层代码。Spring Data Neo4j 支持自动化的 Repository 接口、对象映射、事务管理等功能,大大简化了开发流程。
Spring Data Neo4j 的主要特性包括对象图映射(OGM)、Repository 接口支持、级联操作、事务管理以及与 Spring Boot 的无缝集成。对象图映射允许开发者使用普通的 Java 对象来表示图数据,框架自动处理对象与图之间的转换。Repository 接口提供了声明式的数据访问方法,类似于 Spring Data JPA 的使用方式。级联操作支持节点之间关系的级联保存、更新和删除。事务管理则确保了数据的一致性和完整性。
二、环境准备与项目搭建
2.1 Neo4j 环境安装
在使用 Spring Boot 集成 Neo4j 之前,我们需要先安装并配置 Neo4j 数据库。Neo4j 提供了多种安装方式,包括桌面应用、docker 容器和原生安装。
Neo4j Desktop 是最便捷的本地开发工具,提供了图形化的管理界面。访问 Neo4j 官方网站下载 Neo4j Desktop 安装包,安装完成后创建新的数据库实例即可。首次启动时会设置密码,请务必记住这个密码,后续配置中需要使用。
使用 Docker 容器运行 Neo4j 是另一种常见的方式,特别适合开发测试环境。执行以下命令即可启动 Neo4j 容器:
docker run \
--name neo4j \
-p 7474:7474 \
-p 7687:7687 \
-v neo4j/data:/data \
-d \
neo4j:latest
上述命令会启动一个 Neo4j 容器,将 7474 端口用于 HTTP API 访问,7687 端口用于 Bolt 协议连接。数据会持久化到本地目录中。容器启动后,可以通过浏览器访问 http://localhost:7474 进入 Neo4j Browser 管理界面。
Neo4j Browser 是一个强大的图数据库管理工具,支持 Cypher 查询语句的执行、数据可视化和结果导出等功能。首次登录使用默认用户名 neo4j 和启动时设置的密码。
2.2 Spring Boot 项目初始化
接下来创建 Spring Boot 项目,推荐使用 Spring Initializr 进行项目初始化。访问 https://start.spring.io/,选择以下依赖:
核心依赖包括 Spring Web、Spring Data Neo4j 和 Neo4j Driver。Spring Web 提供 RESTful API 支持,Spring Data Neo4j 是数据访问层的基础,Neo4j Driver 则是与数据库通信的驱动。
如果使用 Maven 构建,在 pom.xml 中添加以下依赖:
XML
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
如果使用 Gradle,构建脚本中添加以下配置:
XML
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
implementation 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
2.3 配置文件配置
在 application.yml 中配置 Neo4j 连接信息:
XML
spring:
data:
neo4j:
uri: bolt://localhost:7687
username: neo4j
password: your_password
database: neo4j
server:
port: 8080
logging:
level:
org.neo4j.ogm: DEBUG
org.springframework.data.neo4j: DEBUG
上述配置中,uri 指定了 Neo4j 的连接地址,Bolt 协议是 Neo4j 官方推荐的高性能连接方式。username 和 password 分别对应数据库的用户名和密码。database 指定要使用的数据库,Neo4j 5.x 版本支持多数据库,默认使用 neo4j。
对于生产环境,建议将敏感信息(如密码)存储在环境变量或配置中心中,而不是直接写在配置文件中:
XML
spring:
data:
neo4j:
uri: ${NEO4J_URI:bolt://localhost:7687}
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD:your_password}
三、实体类设计与对象映射
3.1 节点实体设计
在图数据库中,节点代表实体对象。Spring Data Neo4j 使用注解来定义节点实体,每个实体类对应图中的一个节点类型。
首先创建一个基础节点类,包含所有节点共有的属性:
java
package com.example.neo4j.entity.base;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.GeneratedId;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Node
public abstract class BaseEntity {
@Id
@GeneratedValue(GeneratedValue.class)
private Long id;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public BaseEntity(Long id) {
this.id = id;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
}
BaseEntity 使用 @Node 注解标记为节点实体,包含 id、创建时间和更新时间等通用字段。@Id 注解标识主键字段,@GeneratedValue 注解则指定主键的生成策略。
接下来创建具体的人员节点实体:
java
package com.example.neo4j.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.ToString;
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 java.util.HashSet;
import java.util.Set;
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Node(labels = {"Person", "User"})
public class Person extends BaseEntity {
@Property(name = "name")
private String name;
@Property(name = "age")
private Integer age;
@Property(name = "email")
private String email;
@Property(name = "city")
private String city;
@Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
private Set<Person> knows = new HashSet<>();
@Relationship(type = "WORKS_AT", direction = Relationship.Direction.OUTGOING)
private Company worksAt;
public Person(String name, Integer age, String email, String city) {
super();
this.name = name;
this.age = age;
this.email = email;
this.city = city;
}
public void knows(Person person) {
if (this.knows == null) {
this.knows = new HashSet<>();
}
this.knows.add(person);
}
public void worksAt(Company company) {
this.worksAt = company;
}
}
Person 节点包含姓名、年龄、邮箱和城市等属性。使用 @Property 注解可以自定义属性名称。@Relationship 注解定义了该节点与其他节点的关系,type 指定关系类型,direction 指定关系方向(OUTGOING 表示从当前节点指向目标节点)。
3.2 关系实体设计
在某些场景下,关系本身也需要存储属性。比如"朋友"关系可以包含认识时间、关系强度等信息:
java
package com.example.neo4j.entity.relationship;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@RelationshipProperties
public class KnowsRelationship {
@Id
private Long id;
@TargetNode
private Person person;
private LocalDateTime since;
private Integer strength;
public KnowsRelationship(Person person, LocalDateTime since, Integer strength) {
this.person = person;
this.since = since;
this.strength = strength;
}
}
关系实体使用 @RelationshipProperties 注解标记,其中 @TargetNode 指定关系的终点节点。修改 Person 实体以使用这个关系实体:
java
@Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
private Set<KnowsRelationship> knows = new HashSet<>();
3.3 多样性关系处理
在实际应用中,一个节点可能与多种类型的节点存在关系。以下示例展示了如何在一个实体中定义多种关系类型:
java
package com.example.neo4j.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
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.Relationship.Direction;
import java.util.HashSet;
import java.util.Set;
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Node(labels = {"Company", "Organization"})
public class Company extends BaseEntity {
@Property(name = "name")
private String name;
@Property(name = "industry")
private String industry;
@Property(name = "location")
private String location;
@Property(name = "foundedYear")
private Integer foundedYear;
@Relationship(type = "HAS_MEMBER", direction = Direction.INCOMING)
private Set<Person> employees = new HashSet<>();
@Relationship(type = "PARTNER", direction = Direction.BOTH)
private Set<Company> partners = new HashSet<>();
public Company(String name, String industry, String location, Integer foundedYear) {
super();
this.name = name;
this.industry = industry;
this.location = location;
this.foundedYear = foundedYear;
}
public void addEmployee(Person person) {
if (this.employees == null) {
this.employees = new HashSet<>();
}
this.employees.add(person);
person.worksAt(this);
}
public void partnerWith(Company company) {
if (this.partners == null) {
this.partners = new HashSet<>();
}
this.partners.add(company);
}
}
INCOMING 关系表示关系指向当前节点(员工在公司工作),BOTH 关系则表示双向关系(合作伙伴)。灵活运用这些关系方向可以准确描述现实世界的复杂关系网络。
四、Repository 接口与数据访问层
4.1 基础 Repository 接口
Spring Data Neo4j 提供了类似 Spring Data JPA 的 Repository 接口机制,通过声明式的方式定义数据访问方法:
java
package com.example.neo4j.repository;
import com.example.neo4j.entity.Person;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface PersonRepository extends Neo4jRepository<Person, Long> {
Optional<Person> findByName(String name);
List<Person> findByAgeGreaterThan(Integer age);
List<Person> findByCity(String city);
@Query("MATCH (p:Person) WHERE p.name CONTAINS $name RETURN p")
List<Person> searchByName(@Param("name") String name);
@Query("MATCH (p:Person)-[:KNOWS]->(friend:Person) WHERE ID(p) = $id RETURN friend")
List<Person> findFriends(@Param("id") Long personId);
@Query("MATCH (p:Person)-[:KNOWS]->(f:Person)-[:KNOWS]->(friendOfFriend:Person) " +
"WHERE ID(p) = $id AND NOT (p)-[:KNOWS]->(friendOfFriend) " +
"RETURN DISTINCT friendOfFriend")
List<Person> findFriendsOfFriends(@Param("id") Long personId);
}
上述 Repository 接口继承自 Neo4jRepository,提供了基础的 CRUD 操作方法。同时,接口中还定义了几个自定义查询方法,演示了如何使用 @Query 注解编写 Cypher 查询语句。
4.2 复杂查询方法
针对复杂的业务查询需求,可以编写更高级的 Cypher 查询:
java
@Repository
public interface PersonRepository extends Neo4jRepository<Person, Long> {
@Query("MATCH (p:Person)-[:WORKS_AT]->(c:Company) " +
"WHERE c.name = $companyName " +
"RETURN p ORDER BY p.age DESC")
List<Person> findEmployeesByCompany(@Param("companyName") String companyName);
@Query("MATCH (p1:Person)-[:KNOWS]->(p2:Person) " +
"MATCH (p2:Person)-[:WORKS_AT]->(c:Company) " +
"WHERE p1.name = $personName " +
"RETURN p2, c")
List<Person> findFriendsAndTheirCompanies(@Param("personName") String personName);
@Query("MATCH (p:Person) " +
"OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) " +
"RETURN p, COLLECT(c) AS companies")
List<Person> findAllWithCompanies();
@Query("MATCH (p:Person) " +
"WITH p, size((p)-[:KNOWS]->()) AS friendCount " +
"WHERE friendCount >= $minFriends " +
"RETURN p ORDER BY friendCount DESC " +
"SKIP $skip LIMIT $limit")
List<Person> findPopularPeople(@Param("minFriends") Integer minFriends,
@Param("skip") Integer skip,
@Param("limit") Integer limit);
@Query("MATCH path = (p1:Person)-[:KNOWS*1..3]->(p2:Person) " +
"WHERE p1.name = $startName AND p2.name = $endName " +
"RETURN path, length(path) AS distance " +
"ORDER BY distance ASC LIMIT 1")
List<Person> findShortestPath(@Param("startName") String startName,
@Param("endName") String endName);
}
这些复杂查询展示了 Cypher 语言的各种特性,包括多跳关系查询、路径查找、聚合函数、分页查询等。掌握这些查询技巧可以应对大多数业务场景。
4.3 公司实体 Repository
java
package com.example.neo4j.repository;
import com.example.neo4j.entity.Company;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CompanyRepository extends Neo4jRepository<Company, Long> {
Optional<Company> findByName(String name);
List<Company> findByIndustry(String industry);
@Query("MATCH (c:Company)<-[:WORKS_AT]-(p:Person) " +
"WHERE c.name = $companyName " +
"RETURN c, COLLECT(p) AS employees")
Optional<Company> findByNameWithEmployees(@Param("companyName") String companyName);
@Query("MATCH (c1:Company)-[:PARTNER]->(c2:Company) " +
"WHERE c1.name = $companyName " +
"RETURN c2")
List<Company> findPartners(@Param("companyName") String companyName);
@Query("MATCH (c:Company) " +
"WITH c, size((c)<-[:WORKS_AT]-(:Person)) AS employeeCount " +
"RETURN c ORDER BY employeeCount DESC")
List<Company> findCompaniesByEmployeeCount();
}
五、服务层实现与业务逻辑
5.1 人员服务层实现
创建 PersonService 接口和实现类:
java
package com.example.neo4j.service;
import com.example.neo4j.entity.Person;
import java.util.List;
import java.util.Optional;
public interface PersonService {
Person save(Person person);
Optional<Person> findById(Long id);
List<Person> findAll();
void deleteById(Long id);
List<Person> findFriends(Long personId);
List<Person> findFriendsOfFriends(Long personId);
List<Person> findByName(String name);
List<Person> searchByName(String keyword);
Person createFriendship(Long personId1, Long personId2);
void removeFriendship(Long personId1, Long personId2);
}
package com.example.neo4j.service.impl;
import com.example.neo4j.entity.Person;
import com.example.neo4j.repository.PersonRepository;
import com.example.neo4j.service.PersonService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional
public class PersonServiceImpl implements PersonService {
private final PersonRepository personRepository;
@Override
public Person save(Person person) {
return personRepository.save(person);
}
@Override
@Transactional(readOnly = true)
public Optional<Person> findById(Long id) {
return personRepository.findById(id);
}
@Override
@Transactional(readOnly = true)
public List<Person> findAll() {
return personRepository.findAll();
}
@Override
public void deleteById(Long id) {
personRepository.deleteById(id);
}
@Override
@Transactional(readOnly = true)
public List<Person> findFriends(Long personId) {
return personRepository.findFriends(personId);
}
@Override
@Transactional(readOnly = true)
public List<Person> findFriendsOfFriends(Long personId) {
return personRepository.findFriendsOfFriends(personId);
}
@Override
@Transactional(readOnly = true)
public List<Person> findByName(String name) {
return personRepository.findByName(name);
}
@Override
@Transactional(readOnly = true)
public List<Person> searchByName(String keyword) {
return personRepository.searchByName(keyword);
}
@Override
public Person createFriendship(Long personId1, Long personId2) {
Optional<Person> person1Opt = personRepository.findById(personId1);
Optional<Person> person2Opt = personRepository.findById(personId2);
if (person1Opt.isPresent() && person2Opt.isPresent()) {
Person person1 = person1Opt.get();
Person person2 = person2Opt.get();
person1.knows(person2);
return personRepository.save(person1);
}
throw new RuntimeException("Person not found");
}
@Override
public void removeFriendship(Long personId1, Long personId2) {
// 通过删除和重新保存来移除关系
Optional<Person> person1Opt = personRepository.findById(personId1);
if (person1Opt.isPresent()) {
Person person1 = person1Opt.get();
person1.getKnows().removeIf(p -> p.getId().equals(personId2));
personRepository.save(person1);
}
}
}
服务层封装了业务逻辑,通过调用 Repository 接口完成数据访问。所有修改操作都添加了 @Transactional 注解,确保事务的原子性。
5.2 公司服务层实现
六、控制器层与 RESTful API
6.1 人员控制器
package com.example.neo4j.controller;
import com.example.neo4j.entity.Person;
import com.example.neo4j.service.PersonService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/persons")
@RequiredArgsConstructor
public class PersonController {
private final PersonService personService;
@PostMapping
public ResponseEntity<Person> createPerson(@RequestBody Person person) {
Person saved = personService.save(person);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@GetMapping("/{id}")
public ResponseEntity<Person> getPerson(@PathVariable Long id) {
return personService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<Person>> getAllPersons() {
return ResponseEntity.ok(personService.findAll());
}
@PutMapping("/{id}")
public ResponseEntity<Person> updatePerson(@PathVariable Long id,
@RequestBody Person person) {
return personService.findById(id)
.map(existing -> {
person.setId(id);
return ResponseEntity.ok(personService.save(person));
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePerson(@PathVariable Long id) {
personService.deleteById(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/friends")
public ResponseEntity<List<Person>> getFriends(@PathVariable Long id) {
return ResponseEntity.ok(personService.findFriends(id));
}
@GetMapping("/{id}/friends-of-friends")
public ResponseEntity<List<Person>> getFriendsOfFriends(@PathVariable Long id) {
return ResponseEntity.ok(personService.findFriendsOfFriends(id));
}
@GetMapping("/search")
public ResponseEntity<List<Person>> searchByName(@RequestParam String keyword) {
return ResponseEntity.ok(personService.searchByName(keyword));
}
@PostMapping("/{id1}/friends/{id2}")
public ResponseEntity<Person> createFriendship(@PathVariable Long id1,
@PathVariable Long id2) {
try {
Person person = personService.createFriendship(id1, id2);
return ResponseEntity.ok(person);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
}
}
@DeleteMapping("/{id1}/friends/{id2}")
public ResponseEntity<Void> removeFriendship(@PathVariable Long id1,
@PathVariable Long id2) {
personService.removeFriendship(id1, id2);
return ResponseEntity.noContent().build();
}
}
6.2 公司控制器
package com.example.neo4j.controller;
import com.example.neo4j.entity.Company;
import com.example.neo4j.service.CompanyService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/companies")
@RequiredArgsConstructor
public class CompanyController {
private final CompanyService companyService;
@PostMapping
java
package com.example.neo4j.service;
import com.example.neo4j.entity.Company;
import java.util.List;
import java.util.Optional;
public interface CompanyService {
Company save(Company company);
Optional<Company> findById(Long id);
List<Company> findAll();
void deleteById(Long id);
Optional<Company> findByNameWithEmployees(String name);
List<Company> findPartners(String companyName);
List<Company> findCompaniesByEmployeeCount();
void addEmployee(Long companyId, Long personId);
void removeEmployee(Long companyId, Long personId);
void createPartnership(Long companyId1, Long companyId2);
}
package com.example.neo4j.service.impl;
import com.example.neo4j.entity.Company;
import com.example.neo4j.entity.Person;
import com.example.neo4j.repository.CompanyRepository;
import com.example.neo4j.repository.PersonRepository;
import com.example.neo4j.service.CompanyService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional
public class CompanyServiceImpl implements CompanyService {
private final CompanyRepository companyRepository;
private final PersonRepository personRepository;
@Override
public Company save(Company company) {
return companyRepository.save(company);
}
@Override
@Transactional(readOnly = true)
public Optional<Company> findById(Long id) {
return companyRepository.findById(id);
}
@Override
@Transactional(readOnly = true)
public List<Company> findAll() {
return companyRepository.findAll();
}
@Override
public void deleteById(Long id) {
companyRepository.deleteById(id);
}
@Override
@Transactional(readOnly = true)
public Optional<Company> findByNameWithEmployees(String name) {
return companyRepository.findByNameWithEmployees(name);
}
@Override
@Transactional(readOnly = true)
public List<Company> findPartners(String companyName) {
return companyRepository.findPartners(companyName);
}
@Override
@Transactional(readOnly = true)
public List<Company> findCompaniesByEmployeeCount() {
return companyRepository.findCompaniesByEmployeeCount();
}
@Override
public void addEmployee(Long companyId, Long personId) {
Optional<Company> companyOpt = companyRepository.findById(companyId);
Optional<Person> personOpt = personRepository.findById(personId);
if (companyOpt.isPresent() && personOpt.isPresent()) {
Company company = companyOpt.get();
Person person = personOpt.get();
company.addEmployee(person);
companyRepository.save(company);
}
}
@Override
public void removeEmployee(Long companyId, Long personId) {
Optional<Company> companyOpt = companyRepository.findById(companyId);
if (companyOpt.isPresent()) {
Company company = companyOpt.get();
company.getEmployees().removeIf(p -> p.getId().equals(personId));
companyRepository.save(company);
}
}
@Override
public void createPartnership(Long companyId1, Long companyId2) {
Optional<Company> company1Opt = companyRepository.findById(companyId1);
Optional<Company> company2Opt = companyRepository.findById(companyId2);
if (company1Opt.isPresent() && company2Opt.isPresent()) {
Company company1 = company1Opt.get();
Company company2 = company2Opt.get();
company1.partnerWith(company2);
companyRepository.save(company1);
}
}
}
java
public ResponseEntity<Company> createCompany(@RequestBody Company company) {
Company saved = companyService.save(company);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@GetMapping("/{id}")
public ResponseEntity<Company> getCompany(@PathVariable Long id) {
return companyService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<Company>> getAllCompanies() {
return ResponseEntity.ok(companyService.findAll());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCompany(@PathVariable Long id) {
companyService.deleteById(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/employees")
public ResponseEntity<Company> getCompanyWithEmployees(@PathVariable Long id) {
return companyService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/by-name")
public ResponseEntity<Company> getCompanyByName(@RequestParam String name) {
return companyService.findByNameWithEmployees(name)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/partners")
public ResponseEntity<List<Company>> getPartners(@RequestParam String companyName) {
return ResponseEntity.ok(companyService.findPartners(companyName));
}
@GetMapping("/ranking")
public ResponseEntity<List<Company>> getCompaniesByEmployeeCount() {
return ResponseEntity.ok(companyService.findCompaniesByEmployeeCount());
}
@PostMapping("/{companyId}/employees/{personId}")
public ResponseEntity<Void> addEmployee(@PathVariable Long companyId,
@PathVariable Long personId) {
companyService.addEmployee(companyId, personId);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{companyId}/employees/{personId}")
public ResponseEntity<Void> removeEmployee(@PathVariable Long companyId,
@PathVariable Long personId) {
companyService.removeEmployee(companyId, personId);
return ResponseEntity.noContent().build();
}
@PostMapping("/{id1}/partners/{id2}")
public ResponseEntity<Void> createPartnership(@PathVariable Long id1,
@PathVariable Long id2) {
companyService.createPartnership(id1, id2);
return ResponseEntity.ok().build();
}
}
七、数据初始化与测试
7.1 数据初始化脚本
创建数据初始化类,添加一些示例数据:
java
package com.example.neo4j.config;
import com.example.neo4j.entity.Company;
import com.example.neo4j.entity.Person;
import com.example.neo4j.repository.CompanyRepository;
import com.example.neo4j.repository.PersonRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Component
@RequiredArgsConstructor
@Transactional
public class DataInitializer implements CommandLineRunner {
private final PersonRepository personRepository;
private final CompanyRepository companyRepository;
@Override
public void run(String... args) {
if (personRepository.count() > 0) {
log.info("数据已存在,跳过初始化");
return;
}
log.info("开始初始化示例数据");
// 创建公司
Company google = new Company("Google", "互联网", "美国加州", 1998);
Company meta = new Company("Meta", "互联网", "美国加州", 2004);
Company amazon = new Company("Amazon", "电商", "美国西雅图", 1994);
google = companyRepository.save(google);
meta = companyRepository.save(meta);
amazon = companyRepository.save(amazon);
// 创建人员并建立关系
Person alice = new Person("Alice", 30, "alice@google.com", "旧金山");
Person bob = new Person("Bob", 28, "bob@meta.com", "旧金山");
Person charlie = new Person("Charlie", 35, "charlie@amazon.com", "西雅图");
Person david = new Person("David", 32, "david@google.com", "纽约");
Person eve = new Person("Eve", 26, "eve@meta.com", "纽约");
alice = personRepository.save(alice);
bob = personRepository.save(bob);
charlie = personRepository.save(charlie);
david = personRepository.save(david);
eve = personRepository.save(eve);
// 建立朋友关系
alice.knows(bob);
alice.knows(charlie);
bob.knows(charlie);
bob.knows(david);
charlie.knows(david);
david.knows(eve);
// 建立雇佣关系
alice.worksAt(google);
bob.worksAt(meta);
charlie.worksAt(amazon);
david.worksAt(google);
eve.worksAt(meta);
personRepository.save(alice);
personRepository.save(bob);
personRepository.save(charlie);
personRepository.save(david);
personRepository.save(eve);
// 建立公司合作关系
google.getPartners().add(meta);
meta.getPartners().add(google);
companyRepository.save(google);
log.info("示例数据初始化完成");
}
}
7.2 测试用例
编写单元测试验证功能正确性:
java
package com.example.neo4j;
import com.example.neo4j.entity.Company;
import com.example.neo4j.entity.Person;
import com.example.neo4j.repository.CompanyRepository;
import com.example.neo4j.repository.PersonRepository;
import com.example.neo4j.service.CompanyService;
import com.example.neo4j.service.PersonService;
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.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class Neo4jApplicationTest {
@Autowired
private PersonService personService;
@Autowired
private CompanyService companyService;
@Autowired
private PersonRepository personRepository;
@Autowired
private CompanyRepository companyRepository;
private Person alice;
private Person bob;
private Company google;
@BeforeEach
void setUp() {
personRepository.deleteAll();
companyRepository.deleteAll();
google = new Company("Google", "互联网", "美国加州", 1998);
google = companyRepository.save(google);
alice = new Person("Alice", 30, "alice@test.com", "旧金山");
bob = new Person("Bob", 28, "bob@test.com", "旧金山");
alice = personRepository.save(alice);
bob = personRepository.save(bob);
}
@Test
void testSavePerson() {
Person person = new Person("Charlie", 35, "charlie@test.com", "纽约");
Person saved = personService.save(person);
assertNotNull(saved.getId());
assertEquals("Charlie", saved.getName());
}
@Test
void testFindById() {
Optional<Person> found = personService.findById(alice.getId());
assertTrue(found.isPresent());
assertEquals("Alice", found.get().getName());
}
@Test
void testCreateFriendship() {
Person aliceUpdated = personService.createFriendship(alice.getId(), bob.getId());
List<Person> aliceFriends = personService.findFriends(alice.getId());
assertEquals(1, aliceFriends.size());
assertEquals("Bob", aliceFriends.get(0).getName());
}
@Test
void testFindFriendsOfFriends() {
Person charlie = new Person("Charlie", 35, "charlie@test.com", "纽约");
charlie = personRepository.save(charlie);
alice.knows(bob);
bob.knows(charlie);
personRepository.save(alice);
personRepository.save(bob);
List<Person> fofs = personService.findFriendsOfFriends(alice.getId());
assertEquals(1, fofs.size());
assertEquals("Charlie", fofs.get(0).getName());
}
@Test
void testAddEmployee() {
companyService.addEmployee(google.getId(), alice.getId());
Optional<Company> companyWithEmployees = companyService.findByNameWithEmployees("Google");
assertTrue(companyWithEmployees.isPresent());
assertEquals(1, companyWithEmployees.get().getEmployees().size());
}
@Test
void testFindByName() {
List<Person> results = personService.findByName("Alice");
assertEquals(1, results.size());
assertEquals("Alice", results.get(0).getName());
}
@Test
void testDeletePerson() {
personService.deleteById(alice.getId());
Optional<Person> deleted = personService.findById(alice.getId());
assertFalse(deleted.isPresent());
}
@Test
void testSearchByName() {
Person charlie = new Person("Alexander", 40, "alex@test.com", "波士顿");
personRepository.save(charlie);
List<Person> results = personService.searchByName("Alex");
assertEquals(1, results.size());
}
}
八、最佳实践与性能优化
8.1 实体设计最佳实践
在使用 Spring Data Neo4j 进行实体设计时,需要遵循一些最佳实践以确保代码质量和性能。首先,实体类应该保持简洁,只包含必要的属性。复杂的业务逻辑应该放在服务层处理,而不是实体类中。其次,合理使用继承来共享公共属性,但要避免过深的继承层次。第三,使用合适的注解来精确控制映射行为,如 @Property 的 name 属性可以避免属性名与 Cypher 关键字冲突。
java
// 推荐:清晰的实体设计
@Data
@Node(labels = {"Person", "Developer"})
public class Developer extends BaseEntity {
@Property(name = "name")
private String name;
@Property(name = "github")
private String githubUsername;
@Relationship(type = "KNOWS", direction = OUTGOING)
private Set<Developer> collaborators;
@Relationship(type = "STARRED", direction = OUTGOING)
private Set<Project> starredProjects;
}
// 不推荐:过于复杂的实体设计
@Data
@Node
public class ComplexPerson {
// 混入太多不相关的属性和方法
private String name;
private List<Order> orders;
private Set<Comment> comments;
private Map<String, Object> dynamicProperties;
private List<NestedObject> nestedObjects;
}
8.2 查询优化技巧
编写高效的 Cypher 查询是优化 Neo4j 性能的关键。以下是一些重要的优化技巧:
java
@Repository
public interface PersonRepository extends Neo4jRepository<Person, Long> {
// 坏例子:没有使用索引
@Query("MATCH (p:Person) WHERE p.name = $name RETURN p")
List<Person> findByNameSlow(String name);
// 好例子:利用索引加速查询
// 确保在数据库中创建了索引:CREATE INDEX person_name_index FOR (p:Person) ON (p.name)
// 使用 OPTIONAL MATCH 处理可能不存在的关联
@Query("MATCH (p:Person {name: $name}) " +
"OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) " +
"RETURN p, COLLECT(c) AS companies")
Optional<Person> findByNameWithCompany(String name);
// 使用 LIMIT 限制结果数量
@Query("MATCH (p:Person) RETURN p LIMIT 100")
List<Person> findFirst100();
// 使用 SKIP 和 LIMIT 实现分页
@Query("MATCH (p:Person) RETURN p ORDER BY p.createdAt DESC SKIP $skip LIMIT $limit")
List<Person> findPaginated(@Param("skip") int skip, @Param("limit") int limit);
}
创建索引以加速查询:
java
package com.example.neo4j.config;
import org.neo4j.driver.Driver;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalApplicationListener;
@Configuration
public class Neo4jIndexInitializer implements TransactionalApplicationListener {
private final Driver driver;
public Neo4jIndexInitializer(Driver driver) {
this.driver = driver;
}
@Override
public void handleTransactionEvent(TransactionEvent<ApplicationReadyEvent> event) {
try (var session = driver.session()) {
session.executeWrite(tx -> {
tx.run("CREATE INDEX IF NOT EXISTS person_name_index FOR (p:Person) ON (p.name)");
tx.run("CREATE INDEX IF NOT EXISTS person_email_index FOR (p:Person) ON (p.email)");
tx.run("CREATE INDEX IF NOT EXISTS company_name_index FOR (c:Company) ON (c.name)");
return null;
});
}
}
}
8.3 事务管理与并发控制
正确处理事务是保证数据一致性的关键。Spring Data Neo4j 自动支持 Spring 的事务管理机制:
java
@Service
@RequiredArgsConstructor
@Transactional
public class SocialGraphService {
private final PersonRepository personRepository;
public void batchCreateFriendships(List<Long> personIds) {
// 所有操作在同一个事务中完成
for (int i = 0; i < personIds.size() - 1; i++) {
Person person1 = personRepository.findById(personIds.get(i)).orElseThrow();
Person person2 = personRepository.findById(personIds.get(i + 1)).orElseThrow();
person1.knows(person2);
personRepository.save(person1);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createIndependentOperation(Person person) {
// 独立事务
personRepository.save(person);
}
}
8.4 连接池配置
对于高并发场景,需要调整 Neo4j 连接池配置:
XML
spring:
data:
neo4j:
uri: bolt://localhost:7687
username: neo4j
password: your_password
database: neo4j
# 连接池配置
ogm:
driver:
config:
connection_pool_max: 50
connection_acquisition_timeout: 30s
max_connection_lifetime: 1h
encryption: false
九、总结与扩展阅读
本文详细介绍了 Spring Boot 集成 Neo4j 图数据库的完整方案,涵盖了从环境搭建到实际应用的各个方面。通过本文的学习,读者应该能够掌握图数据库的基本概念、Spring Data Neo4j 的使用方法,以及在实际项目中应用图数据库的技巧。
在实际开发中,图数据库特别适合以下场景:社交网络分析、推荐系统、欺诈检测、知识图谱、路径规划等。对于关系复杂且需要深度遍历的数据,图数据库往往能够提供比关系型数据库更好的性能。
如果希望进一步深入学习,建议阅读以下资源:Neo4j 官方文档提供了完整的 Cypher 查询语言参考;Spring Data Neo4j 官方文档详细介绍了框架的使用方法;《Graph Databases》一书深入讲解了图数据模型的设计原则;《Neo4j in Action》是实战性很强的进阶读物。
掌握图数据库技术将为你的职业发展增添重要的竞争力,也能够帮助你构建更加高效和优雅的数据驱动型应用程序。