在处理社交网络、推荐系统或知识图谱等复杂关系数据时,传统的关系型数据库常常显得力不从心。本文将带你深入实践,在 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 WebSpring Data Neo4jNeo4j 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 语句简洁得多。

[AFFILIATE_SLOT_1]

五、构建服务与 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;
}