在处理社交网络、推荐系统或知识图谱等复杂关系数据时,传统的关系型数据库常常显得力不从心。本文将带你深入实践,在 Spring Boot 项目中集成 Neo4j 图数据库,通过一个完整的案例,掌握如何利用图模型高效处理多跳关系查询,为你的 Java 应用注入处理复杂关系的强大能力。
一、图数据库与 Neo4j:为何是处理复杂关系的利器
当你的业务涉及“朋友的朋友的朋友”这类多层级关系查询时,传统 SQL 的多表 JOIN 操作性能会急剧下降。而图数据库,如 Neo4j,将关系作为一等公民,其性能在处理深度遍历时具有显著优势。这就像在 Java、Python 或 Go 等语言生态中选择合适的工具一样,针对关系密集型场景,Neo4j 是更自然的选择。
Neo4j 的核心优势在于其直观的模型:
- 节点(Node):代表实体,如用户、产品。
- 关系(Relationship):连接节点,定义它们之间的关联,如“关注”、“购买”。
- 属性(Property):存储节点和关系的键值对信息。
Spring Data Neo4j 作为 Spring Data 家族成员,提供了类似 JPA 的编程体验,让你能用熟悉的注解和 Repository 模式操作图数据,极大简化了开发,无论你之前是使用 Spring Data JPA 操作 MySQL,还是用其他 ORM 框架。
二、快速搭建开发环境:从零启动项目
开始之前,你需要一个运行中的 Neo4j 实例。对于本地开发,Docker 是最便捷的方式之一。运行以下命令即可启动一个 Neo4j 容器:
docker run \
--name neo4j \
-p 7474:7474 \
-p 7687:7687 \
-v neo4j/data:/data \
-d \
neo4j:latest
启动后,访问 http://localhost:7474 即可使用 Neo4j Browser 进行可视化管理和查询。
接下来,使用 Spring Initializr 创建一个新的 Spring Boot 项目。添加 Spring Web、Spring Data Neo4j 和 Neo4j Driver 依赖。Maven 用户需要在 pom.xml 中添加:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-neo4j
org.neo4j
neo4j-ogm-core
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
Gradle 用户的配置如下:
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'
}
随后,在 application.yml 中配置数据库连接信息,建议将密码等敏感信息通过环境变量管理:
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
spring:
data:
neo4j:
uri: ${NEO4J_URI:bolt://localhost:7687}
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD:your_password}
三、定义数据模型:用注解映射节点与关系
Spring Data Neo4j 使用注解将 POJO 映射到图中的节点和关系。首先,定义一个包含公共字段的基类:
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();
}
}
然后,创建具体的业务实体,例如一个“人员”节点,它可以拥有朋友、同事等多种关系:
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 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;
}
}
关键点:@Relationship 注解的 direction 属性(OUTGOING, INCOMING, BOTH)对于准确描述关系方向至关重要。
如果关系本身需要携带属性(如“认识时间”),则需要定义关系实体:
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;
}
}
并在 Person 实体中引用它:
@Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
private Set knows = new HashSet<>();
这种设计模式非常灵活,能够满足各种复杂的业务场景,其思想与在 TypeScript 中定义接口或在 C++ 中定义结构体以增强类型安全有异曲同工之妙。
四、数据访问层:使用 Repository 和 Cypher 查询
Spring Data Neo4j 提供了强大的 Repository 支持。你可以像使用 JPA 一样,通过继承 Neo4jRepository 接口获得基础的 CRUD 方法。
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 {
Optional findByName(String name);
List findByAgeGreaterThan(Integer age);
List findByCity(String city);
@Query("MATCH (p:Person) WHERE p.name CONTAINS $name RETURN p")
List searchByName(@Param("name") String name);
@Query("MATCH (p:Person)-[:KNOWS]->(friend:Person) WHERE ID(p) = $id RETURN friend")
List 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 findFriendsOfFriends(@Param("id") Long personId);
}
对于复杂查询,可以直接使用 @Query 注解编写 Cypher 语句。Cypher 是 Neo4j 的声明式查询语言,非常直观易读。
@Repository
public interface PersonRepository extends Neo4jRepository {
@Query("MATCH (p:Person)-[:WORKS_AT]->(c:Company) " +
"WHERE c.name = $companyName " +
"RETURN p ORDER BY p.age DESC")
List 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 findFriendsAndTheirCompanies(@Param("personName") String personName);
@Query("MATCH (p:Person) " +
"OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) " +
"RETURN p, COLLECT(c) AS companies")
List 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 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 findShortestPath(@Param("startName") String startName,
@Param("endName") String endName);
}
例如,查找所有人的朋友的朋友(二度人脉),用 Cypher 表达非常简单:MATCH (p:Person)-[:FRIEND]->(:Person)-[:FRIEND]->(fof) RETURN p.name, collect(fof.name)。这比等价的 SQL 语句简洁得多。
五、构建服务与 API:完成业务闭环
服务层负责封装业务逻辑,并通常使用 @Transactional 注解来保证数据操作的事务性。
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 findById(Long id);
List findAll();
void deleteById(Long id);
List findFriends(Long personId);
List findFriendsOfFriends(Long personId);
List findByName(String name);
List 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 findById(Long id) {
return personRepository.findById(id);
}
@Override
@Transactional(readOnly = true)
public List findAll() {
return personRepository.findAll();
}
@Override
public void deleteById(Long id) {
personRepository.deleteById(id);
}
@Override
@Transactional(readOnly = true)
public List findFriends(Long personId) {
return personRepository.findFriends(personId);
}
@Override
@Transactional(readOnly = true)
public List findFriendsOfFriends(Long personId) {
return personRepository.findFriendsOfFriends(personId);
}
@Override
@Transactional(readOnly = true)
public List findByName(String name) {
return personRepository.findByName(name);
}
@Override
@Transactional(readOnly = true)
public List searchByName(String keyword) {
return personRepository.searchByName(keyword);
}
@Override
public Person createFriendship(Long personId1, Long personId2) {
Optional person1Opt = personRepository.findById(personId1);
Optional 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 person1Opt = personRepository.findById(personId1);
if (person1Opt.isPresent()) {
Person person1 = person1Opt.get();
person1.getKnows().removeIf(p -> p.getId().equals(personId2));
personRepository.save(person1);
}
}
}
控制器层则暴露 RESTful API 供前端或其他服务调用。以下是一个人员控制器的示例:
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 createPerson(@RequestBody Person person) {
Person saved = personService.save(person);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@GetMapping("/{id}")
public ResponseEntity getPerson(@PathVariable Long id) {
return personService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity> getAllPersons() {
return ResponseEntity.ok(personService.findAll());
}
@PutMapping("/{id}")
public ResponseEntity 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 deletePerson(@PathVariable Long id) {
personService.deleteById(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/friends")
public ResponseEntity> getFriends(@PathVariable Long id) {
return ResponseEntity.ok(personService.findFriends(id));
}
@GetMapping("/{id}/friends-of-friends")
public ResponseEntity> getFriendsOfFriends(@PathVariable Long id) {
return ResponseEntity.ok(personService.findFriendsOfFriends(id));
}
@GetMapping("/search")
public ResponseEntity> searchByName(@RequestParam String keyword) {
return ResponseEntity.ok(personService.searchByName(keyword));
}
@PostMapping("/{id1}/friends/{id2}")
public ResponseEntity 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 removeFriendship(@PathVariable Long id1,
@PathVariable Long id2) {
personService.removeFriendship(id1, id2);
return ResponseEntity.noContent().build();
}
}
通过组合服务层方法,可以轻松实现诸如“为指定用户推荐可能认识的人”(基于共同朋友)等复杂业务功能。
六、进阶技巧与性能优化指南
要让应用在生产环境中表现良好,需要关注以下几点:
1. 索引是性能的基石:为经常用于查询条件的属性创建索引,可以极大提升查询速度。
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 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;
});
}
}
}
2. 编写高效的 Cypher 查询:
- 使用参数化查询防止注入并利用缓存。
- 在关系上添加类型限制,缩小遍历范围。
- 避免返回整个路径,只返回需要的节点或属性。
@Repository
public interface PersonRepository extends Neo4jRepository {
// 坏例子:没有使用索引
@Query("MATCH (p:Person) WHERE p.name = $name RETURN p")
List 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 findByNameWithCompany(String name);
// 使用 LIMIT 限制结果数量
@Query("MATCH (p:Person) RETURN p LIMIT 100")
List findFirst100();
// 使用 SKIP 和 LIMIT 实现分页
@Query("MATCH (p:Person) RETURN p ORDER BY p.createdAt DESC SKIP $skip LIMIT $limit")
List findPaginated(@Param("skip") int skip, @Param("limit") int limit);
}
3. 连接池与配置优化:针对高并发场景,调整连接池配置至关重要。
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
4. 事务管理:Spring Data Neo4j 完美集成 Spring 事务管理。确保在修改数据的服务方法上使用 @Transactional。
@Service
@RequiredArgsConstructor
@Transactional
public class SocialGraphService {
private final PersonRepository personRepository;
public void batchCreateFriendships(List 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);
}
}
[AFFILIATE_SLOT_2]
总结
通过本文的实践,你已经掌握了在 Spring Boot 中集成 Neo4j 的核心技能:从环境搭建、实体映射、Repository 查询到服务封装。图数据库在社交网络分析、实时推荐、欺诈检测和知识图谱等领域具有不可替代的优势。当你的数据关系变得复杂时,考虑引入 Neo4j 这样的图数据库,就像在合适的场景下选择 Go 处理高并发、选择 Python 进行数据分析一样,是架构上的明智之举。下一步,可以尝试将本文的示例应用到更复杂的业务模型中,深入探索 Cypher 查询语言的强大功能。
(以下为原文中其他必要的代码占位符,根据上下文逻辑保留,在完整文章中它们会出现在对应的小节内。)
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 employees = new HashSet<>();
@Relationship(type = "PARTNER", direction = Direction.BOTH)
private Set 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);
}
}
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 {
Optional findByName(String name);
List findByIndustry(String industry);
@Query("MATCH (c:Company)<-[:WORKS_AT]-(p:Person) " +
"WHERE c.name = $companyName " +
"RETURN c, COLLECT(p) AS employees")
Optional findByNameWithEmployees(@Param("companyName") String companyName);
@Query("MATCH (c1:Company)-[:PARTNER]->(c2:Company) " +
"WHERE c1.name = $companyName " +
"RETURN c2")
List 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 findCompaniesByEmployeeCount();
}
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
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 findById(Long id);
List findAll();
void deleteById(Long id);
Optional findByNameWithEmployees(String name);
List findPartners(String companyName);
List 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 findById(Long id) {
return companyRepository.findById(id);
}
@Override
@Transactional(readOnly = true)
public List findAll() {
return companyRepository.findAll();
}
@Override
public void deleteById(Long id) {
companyRepository.deleteById(id);
}
@Override
@Transactional(readOnly = true)
public Optional findByNameWithEmployees(String name) {
return companyRepository.findByNameWithEmployees(name);
}
@Override
@Transactional(readOnly = true)
public List findPartners(String companyName) {
return companyRepository.findPartners(companyName);
}
@Override
@Transactional(readOnly = true)
public List findCompaniesByEmployeeCount() {
return companyRepository.findCompaniesByEmployeeCount();
}
@Override
public void addEmployee(Long companyId, Long personId) {
Optional companyOpt = companyRepository.findById(companyId);
Optional 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 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 company1Opt = companyRepository.findById(companyId1);
Optional company2Opt = companyRepository.findById(companyId2);
if (company1Opt.isPresent() && company2Opt.isPresent()) {
Company company1 = company1Opt.get();
Company company2 = company2Opt.get();
company1.partnerWith(company2);
companyRepository.save(company1);
}
}
}
public ResponseEntity createCompany(@RequestBody Company company) {
Company saved = companyService.save(company);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@GetMapping("/{id}")
public ResponseEntity getCompany(@PathVariable Long id) {
return companyService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity> getAllCompanies() {
return ResponseEntity.ok(companyService.findAll());
}
@DeleteMapping("/{id}")
public ResponseEntity deleteCompany(@PathVariable Long id) {
companyService.deleteById(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/employees")
public ResponseEntity getCompanyWithEmployees(@PathVariable Long id) {
return companyService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/by-name")
public ResponseEntity getCompanyByName(@RequestParam String name) {
return companyService.findByNameWithEmployees(name)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/partners")
public ResponseEntity> getPartners(@RequestParam String companyName) {
return ResponseEntity.ok(companyService.findPartners(companyName));
}
@GetMapping("/ranking")
public ResponseEntity> getCompaniesByEmployeeCount() {
return ResponseEntity.ok(companyService.findCompaniesByEmployeeCount());
}
@PostMapping("/{companyId}/employees/{personId}")
public ResponseEntity addEmployee(@PathVariable Long companyId,
@PathVariable Long personId) {
companyService.addEmployee(companyId, personId);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{companyId}/employees/{personId}")
public ResponseEntity removeEmployee(@PathVariable Long companyId,
@PathVariable Long personId) {
companyService.removeEmployee(companyId, personId);
return ResponseEntity.noContent().build();
}
@PostMapping("/{id1}/partners/{id2}")
public ResponseEntity createPartnership(@PathVariable Long id1,
@PathVariable Long id2) {
companyService.createPartnership(id1, id2);
return ResponseEntity.ok().build();
}
}
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("示例数据初始化完成");
}
}
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 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 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 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 companyWithEmployees = companyService.findByNameWithEmployees("Google");
assertTrue(companyWithEmployees.isPresent());
assertEquals(1, companyWithEmployees.get().getEmployees().size());
}
@Test
void testFindByName() {
List results = personService.findByName("Alice");
assertEquals(1, results.size());
assertEquals("Alice", results.get(0).getName());
}
@Test
void testDeletePerson() {
personService.deleteById(alice.getId());
Optional deleted = personService.findById(alice.getId());
assertFalse(deleted.isPresent());
}
@Test
void testSearchByName() {
Person charlie = new Person("Alexander", 40, "alex@test.com", "波士顿");
personRepository.save(charlie);
List results = personService.searchByName("Alex");
assertEquals(1, results.size());
}
}
// 推荐:清晰的实体设计
@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 collaborators;
@Relationship(type = "STARRED", direction = OUTGOING)
private Set starredProjects;
}
// 不推荐:过于复杂的实体设计
@Data
@Node
public class ComplexPerson {
// 混入太多不相关的属性和方法
private String name;
private List orders;
private Set comments;
private Map dynamicProperties;
private List nestedObjects;
}
浙公网安备 33010602011771号