Spring Data Neo4j - Quick Reference
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:spring-data-neo4jfor comprehensive documentation.
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
Configuration
application.yml
spring:
neo4j:
uri: bolt://localhost:7687
authentication:
username: neo4j
password: ${NEO4J_PASSWORD}
data:
neo4j:
database: mydb # Neo4j 4.0+
Graph Concepts
┌─────────────────────────────────────────────────────────────┐
│ Graph Model │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Person │─────FOLLOWS────────▶│ Person │ │
│ │ (John) │ │ (Jane) │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ WORKS_AT WORKS_AT │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Company │◀─────KNOWS───────────│ Person │ │
│ │ (Acme) │ │ (Bob) │ │
│ └─────────┘ └─────────┘ │
│ │
│ Nodes: Person, Company │
│ Relationships: FOLLOWS, WORKS_AT, KNOWS │
└─────────────────────────────────────────────────────────────┘
Node Entities
@Node("Person")
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
private LocalDate birthDate;
// Outgoing relationship
@Relationship(type = "FOLLOWS", direction = Direction.OUTGOING)
private Set<Person> following = new HashSet<>();
// Incoming relationship
@Relationship(type = "FOLLOWS", direction = Direction.INCOMING)
private Set<Person> followers = new HashSet<>();
// Relationship with properties
@Relationship(type = "WORKS_AT")
private WorksAt employment;
// Multiple relationships of same type
@Relationship(type = "KNOWS")
private List<Knows> connections = new ArrayList<>();
}
@Node("Company")
public class Company {
@Id
@GeneratedValue
private Long id;
private String name;
private String industry;
@Relationship(type = "WORKS_AT", direction = Direction.INCOMING)
private Set<Person> employees = new HashSet<>();
}
Relationship Entities
@RelationshipProperties
public class WorksAt {
@Id
@GeneratedValue
private Long id;
@TargetNode
private Company company;
private String position;
private LocalDate startDate;
private LocalDate endDate;
private BigDecimal salary;
}
@RelationshipProperties
public class Knows {
@Id
@GeneratedValue
private Long id;
@TargetNode
private Person person;
private String context; // "work", "school", "family"
private LocalDate since;
private Integer trustLevel;
}
Repository Pattern
public interface PersonRepository extends Neo4jRepository<Person, Long> {
// Derived queries
Optional<Person> findByEmail(String email);
List<Person> findByNameContaining(String name);
// Custom Cypher queries
@Query("MATCH (p:Person)-[:FOLLOWS]->(f:Person) WHERE p.id = $personId RETURN f")
List<Person> findFollowing(Long personId);
@Query("MATCH (p:Person)<-[:FOLLOWS]-(f:Person) WHERE p.id = $personId RETURN f")
List<Person> findFollowers(Long personId);
@Query("""
MATCH (p:Person {id: $personId})-[:FOLLOWS*2..3]->(fof:Person)
WHERE NOT (p)-[:FOLLOWS]->(fof) AND p <> fof
RETURN DISTINCT fof
LIMIT $limit
""")
List<Person> findFriendsOfFriends(Long personId, int limit);
@Query("""
MATCH (p1:Person {id: $person1Id}), (p2:Person {id: $person2Id}),
path = shortestPath((p1)-[:KNOWS*]-(p2))
RETURN path
""")
List<Person> findShortestPath(Long person1Id, Long person2Id);
// Aggregations
@Query("""
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
RETURN c.name as company, count(p) as employeeCount
ORDER BY employeeCount DESC
""")
List<CompanyStats> getCompanyStats();
// With relationship properties
@Query("""
MATCH (p:Person)-[w:WORKS_AT]->(c:Company)
WHERE p.id = $personId
RETURN p, w, c
""")
Person findWithEmployment(Long personId);
}
public interface CompanyRepository extends Neo4jRepository<Company, Long> {
@Query("""
MATCH (c:Company)<-[:WORKS_AT]-(p:Person)
WHERE c.id = $companyId
RETURN p
""")
List<Person> findEmployees(Long companyId);
}
Neo4jTemplate Operations
@Service
@RequiredArgsConstructor
public class GraphService {
private final Neo4jTemplate neo4jTemplate;
private final Neo4jClient neo4jClient;
// Save operations
public Person savePerson(Person person) {
return neo4jTemplate.save(person);
}
// Find by ID
public Optional<Person> findById(Long id) {
return neo4jTemplate.findById(id, Person.class);
}
// Custom queries with Neo4jClient
public List<Map<String, Object>> findMutualConnections(Long person1Id, Long person2Id) {
return neo4jClient.query("""
MATCH (p1:Person {id: $person1Id})-[:KNOWS]-(mutual:Person)-[:KNOWS]-(p2:Person {id: $person2Id})
RETURN mutual.name as name, mutual.email as email
""")
.bind(person1Id).to("person1Id")
.bind(person2Id).to("person2Id")
.fetch()
.all()
.stream()
.toList();
}
// Create relationship
public void createFollowRelationship(Long followerId, Long followeeId) {
neo4jClient.query("""
MATCH (a:Person {id: $followerId}), (b:Person {id: $followeeId})
MERGE (a)-[:FOLLOWS]->(b)
""")
.bind(followerId).to("followerId")
.bind(followeeId).to("followeeId")
.run();
}
// Delete relationship
public void removeFollowRelationship(Long followerId, Long followeeId) {
neo4jClient.query("""
MATCH (a:Person {id: $followerId})-[r:FOLLOWS]->(b:Person {id: $followeeId})
DELETE r
""")
.bind(followerId).to("followerId")
.bind(followeeId).to("followeeId")
.run();
}
// Complex graph traversal
public List<Person> findInfluencers(int minFollowers) {
return neo4jClient.query("""
MATCH (p:Person)<-[:FOLLOWS]-(follower:Person)
WITH p, count(follower) as followerCount
WHERE followerCount >= $minFollowers
RETURN p
ORDER BY followerCount DESC
""")
.bind(minFollowers).to("minFollowers")
.fetchAs(Person.class)
.mappedBy((typeSystem, record) -> {
// Custom mapping if needed
return neo4jTemplate.findById(
record.get("p").asNode().id(),
Person.class
).orElse(null);
})
.all()
.stream()
.filter(Objects::nonNull)
.toList();
}
}
Projections
// Interface projection
public interface PersonSummary {
String getName();
String getEmail();
int getFollowerCount();
}
// DTO projection
public record PersonDto(
Long id,
String name,
String email,
List<String> followerNames
) {}
public interface PersonRepository extends Neo4jRepository<Person, Long> {
@Query("""
MATCH (p:Person)
WHERE p.id = $id
OPTIONAL MATCH (p)<-[:FOLLOWS]-(f:Person)
RETURN p.id as id, p.name as name, p.email as email,
collect(f.name) as followerNames
""")
Optional<PersonDto> findPersonDtoById(Long id);
}
Transactions
@Service
@Transactional
public class SocialNetworkService {
private final PersonRepository personRepository;
private final Neo4jClient neo4jClient;
@Transactional
public void transferFollowers(Long fromPersonId, Long toPersonId) {
// All operations in single transaction
neo4jClient.query("""
MATCH (from:Person {id: $fromId})<-[r:FOLLOWS]-(follower:Person)
MATCH (to:Person {id: $toId})
CREATE (follower)-[:FOLLOWS]->(to)
DELETE r
""")
.bind(fromPersonId).to("fromId")
.bind(toPersonId).to("toId")
.run();
}
@Transactional(readOnly = true)
public List<Person> findRecommendations(Long personId) {
// Read-only transaction
return personRepository.findFriendsOfFriends(personId, 10);
}
}
Reactive Support
public interface ReactivePersonRepository extends ReactiveNeo4jRepository<Person, Long> {
Flux<Person> findByNameContaining(String name);
@Query("MATCH (p:Person)-[:FOLLOWS]->(f:Person) WHERE p.id = $personId RETURN f")
Flux<Person> findFollowing(Long personId);
}
@Service
public class ReactiveGraphService {
private final ReactiveNeo4jClient neo4jClient;
public Flux<Person> streamInfluencers() {
return neo4jClient.query("""
MATCH (p:Person)<-[:FOLLOWS]-(f:Person)
WITH p, count(f) as followers
WHERE followers > 100
RETURN p
ORDER BY followers DESC
""")
.fetchAs(Person.class)
.all();
}
}
Testing with Testcontainers
@SpringBootTest
@Testcontainers
class PersonRepositoryTest {
@Container
static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5")
.withAdminPassword("password");
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", neo4j::getBoltUrl);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", neo4j::getAdminPassword);
}
@Autowired
private PersonRepository personRepository;
@Test
void shouldFindFollowers() {
Person john = personRepository.save(new Person("John"));
Person jane = personRepository.save(new Person("Jane"));
john.getFollowing().add(jane);
personRepository.save(john);
List<Person> followers = personRepository.findFollowers(jane.getId());
assertThat(followers).contains(john);
}
}
Best Practices
| Do | Don't | |----|-------| | Model relationships explicitly | Use arrays for connections | | Use projections for partial data | Fetch entire graph | | Index frequently queried properties | Query without indexes | | Use MERGE for idempotent creates | CREATE duplicates | | Limit traversal depth | Unbounded graph traversals |
Production Checklist
- [ ] Indexes on lookup properties
- [ ] Constraints for uniqueness
- [ ] Connection pooling configured
- [ ] Transaction timeouts set
- [ ] Query profiling enabled
- [ ] Backup strategy defined
- [ ] Cluster configuration (if HA)
- [ ] Memory settings tuned
- [ ] Monitoring enabled
- [ ] Cypher query optimization
When NOT to Use This Skill
- Raw Cypher queries - Consult Neo4j documentation directly
- Relational data - Use
spring-data-jpafor tabular data - Document storage - Use
spring-data-mongodb - Simple key-value - Use
spring-data-redis
Anti-Patterns
| Anti-Pattern | Problem | Solution | |--------------|---------|----------| | Fetching entire graph | Memory issues | Use projections, limit depth | | CREATE instead of MERGE | Duplicate nodes | Use MERGE for idempotent creates | | Unbounded traversals | Performance issues | Add depth limits | | Missing indexes | Slow lookups | Create indexes on lookup properties | | Arrays for relationships | Loses graph benefits | Use proper @Relationship | | Ignoring relationship direction | Wrong query results | Specify INCOMING/OUTGOING |
Quick Troubleshooting
| Problem | Diagnostic | Fix | |---------|------------|-----| | Connection refused | Check Neo4j running | Start Neo4j, check bolt URI | | Node not persisted | Check @Node annotation | Add annotation, verify ID | | Relationship missing | Check @Relationship | Verify type and direction | | Slow Cypher | Use PROFILE/EXPLAIN | Add indexes, optimize query | | Circular reference | Check entity graph | Use @Relationship carefully |
Scan to join WeChat group