19 KiB
Spring Data Neo4j - Reference Guide
This document provides detailed reference information for Spring Data Neo4j, including annotations, query language syntax, configuration options, and API documentation.
Table of Contents
- Annotations Reference
- Cypher Query Language
- Configuration Properties
- Repository Methods
- Projections and DTOs
- Transaction Management
- Performance Tuning
Annotations Reference
Entity Annotations
@Node
Marks a class as a Neo4j node entity.
@Node // Label defaults to class name
@Node("CustomLabel") // Explicit label
@Node({"Label1", "Label2"}) // Multiple labels
public class MyEntity {
// ...
}
Properties:
valueorlabels: String or String array for node labelsprimaryLabel: Specify which label is primary (when using multiple labels)
@Id
Marks a field as the entity identifier.
@Id
private String businessKey; // Custom business key
@Id @GeneratedValue
private Long id; // Auto-generated internal ID
Important:
- Required on every @Node entity
- Can be used with business keys or generated values
- Must be unique within the node type
@GeneratedValue
Configures ID generation strategy.
@Id @GeneratedValue
private Long id; // Uses Neo4j internal ID
@Id @GeneratedValue(generatorClass = UUIDStringGenerator.class)
private String uuid; // Custom UUID generator
@Id @GeneratedValue(generatorClass = MyCustomGenerator.class)
private String customId;
Built-in Generators:
InternalIdGenerator(default for Long): Uses Neo4j's internal IDUUIDStringGenerator: Generates UUID strings
@Property
Maps a field to a different property name in Neo4j.
@Property("graph_property_name")
private String javaFieldName;
When to use:
- Field name differs from graph property name
- Property names contain special characters
- Following different naming conventions
@Relationship
Defines relationships between nodes.
@Relationship(type = "RELATIONSHIP_TYPE", direction = Direction.OUTGOING)
private RelatedEntity related;
@Relationship(type = "RELATED_TO", direction = Direction.INCOMING)
private List<RelatedEntity> incoming;
@Relationship(type = "CONNECTED", direction = Direction.UNDIRECTED)
private Set<RelatedEntity> connections;
Properties:
type(required): Relationship type in Neo4jdirection: OUTGOING, INCOMING, or UNDIRECTED- Default direction is OUTGOING if not specified
Direction Guidelines:
OUTGOING: This node → target nodeINCOMING: Target node → this nodeUNDIRECTED: Ignores direction when querying
@RelationshipProperties
Marks a class as relationship properties container.
@RelationshipProperties
public class ActedIn {
@Id @GeneratedValue
private Long id;
@TargetNode
private Movie movie;
private List<String> roles;
private Integer screenTime;
}
Required Fields:
@Idfield (can be generated)@TargetNodefield pointing to target entity
Repository Annotations
@Query
Defines custom Cypher query for a repository method.
@Query("MATCH (n:Node) WHERE n.property = $param RETURN n")
List<Node> customQuery(@Param("param") String param);
@Query("MATCH (n:Node) WHERE n.id = $0 RETURN n")
Node findById(String id); // Positional parameter
Parameter Binding:
- Use
$paramNamefor named parameters with@Param - Use
$0,$1, etc. for positional parameters - SpEL expressions supported:
#{#entityName}
@Param
Binds method parameter to query parameter.
@Query("MATCH (n) WHERE n.name = $customName RETURN n")
List<Node> find(@Param("customName") String name);
When required:
- Parameter name in query differs from method parameter
- Making intent explicit and clear
Configuration Annotations
@EnableNeo4jRepositories
Enables Neo4j repository support.
@Configuration
@EnableNeo4jRepositories(basePackages = "com.example.repositories")
public class Neo4jConfiguration {
// ...
}
Properties:
basePackages: Packages to scan for repositoriesbasePackageClasses: Type-safe package specificationrepositoryImplementationPostfix: Custom implementation suffix (default: "Impl")
Note: Auto-enabled by Spring Boot starter, manual configuration rarely needed.
@DataNeo4jTest
Test slice annotation for Neo4j tests.
@DataNeo4jTest
class MyRepositoryTest {
@Autowired
private MyRepository repository;
}
What it does:
- Configures test slice for Spring Data Neo4j
- Loads only Neo4j-related beans
- Configures embedded test database when available
- Enables transaction rollback for tests
Cypher Query Language
Basic Patterns
MATCH - Find Patterns
// Find all nodes with label
MATCH (n:Label) RETURN n
// Find node with property
MATCH (n:Label {property: 'value'}) RETURN n
// Find nodes with WHERE clause
MATCH (n:Label) WHERE n.property > 100 RETURN n
// Multiple labels
MATCH (n:Label1:Label2) RETURN n
Relationship Patterns
// Outgoing relationship
MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b
// Incoming relationship
MATCH (a:Person)<-[:KNOWS]-(b:Person) RETURN a, b
// Undirected relationship
MATCH (a:Person)-[:KNOWS]-(b:Person) RETURN a, b
// Relationship with properties
MATCH (a)-[r:KNOWS {since: 2020}]->(b) RETURN a, r, b
// Variable length relationships
MATCH (a)-[:KNOWS*1..3]->(b) RETURN a, b
CREATE - Create Patterns
// Create single node
CREATE (n:Person {name: 'John', age: 30})
// Create node and relationship
CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})
// Create relationship between existing nodes
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS {since: 2020}]->(b)
MERGE - Find or Create
// Find or create node
MERGE (n:Person {email: 'john@example.com'})
ON CREATE SET n.created = timestamp()
ON MATCH SET n.accessed = timestamp()
// Find or create relationship
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
MERGE (a)-[r:KNOWS]->(b)
ON CREATE SET r.since = 2020
SET - Update Properties
// Set single property
MATCH (n:Person {name: 'John'})
SET n.age = 31
// Set multiple properties
MATCH (n:Person {name: 'John'})
SET n.age = 31, n.city = 'London'
// Set from map
MATCH (n:Person {name: 'John'})
SET n += {age: 31, city: 'London'}
// Add label
MATCH (n:Person {name: 'John'})
SET n:Premium
DELETE and REMOVE
// Delete node (must have no relationships)
MATCH (n:Person {name: 'John'})
DELETE n
// Delete node and relationships
MATCH (n:Person {name: 'John'})
DETACH DELETE n
// Delete relationship
MATCH (a)-[r:KNOWS]->(b)
WHERE a.name = 'Alice' AND b.name = 'Bob'
DELETE r
// Remove property
MATCH (n:Person {name: 'John'})
REMOVE n.age
// Remove label
MATCH (n:Person {name: 'John'})
REMOVE n:Premium
Advanced Patterns
Collections and List Functions
// Collect results
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
RETURN p.name, collect(m.title) AS movies
// Unwind collection
UNWIND [1, 2, 3] AS number
RETURN number
// List comprehension
MATCH (p:Person)
RETURN [x IN p.skills WHERE x STARTS WITH 'Java'] AS javaSkills
// Size of collection
MATCH (p:Person)
RETURN p.name, size(p.skills) AS skillCount
Aggregation Functions
// Count
MATCH (p:Person) RETURN count(p)
// Sum
MATCH (p:Product) RETURN sum(p.price)
// Average
MATCH (p:Product) RETURN avg(p.price)
// Min/Max
MATCH (p:Product) RETURN min(p.price), max(p.price)
// Group by with aggregation
MATCH (p:Person)-[:LIVES_IN]->(c:City)
RETURN c.name, count(p) AS population
ORDER BY population DESC
Conditional Logic
// CASE expression
MATCH (p:Person)
RETURN p.name,
CASE
WHEN p.age < 18 THEN 'Minor'
WHEN p.age < 65 THEN 'Adult'
ELSE 'Senior'
END AS category
// COALESCE - first non-null value
MATCH (p:Person)
RETURN coalesce(p.nickname, p.name) AS displayName
Pattern Comprehension
// Pattern comprehension
MATCH (p:Person)
RETURN p.name,
[(p)-[:KNOWS]->(friend) | friend.name] AS friends
// With filtering
MATCH (p:Person)
RETURN p.name,
[(p)-[:KNOWS]->(friend) WHERE friend.age > 30 | friend.name] AS olderFriends
Query Optimization
Using Indexes
// Create index (admin query, not in @Query)
CREATE INDEX person_name FOR (n:Person) ON (n.name)
// Composite index
CREATE INDEX person_name_age FOR (n:Person) ON (n.name, n.age)
// Use index hint
MATCH (p:Person)
USING INDEX p:Person(name)
WHERE p.name = 'John'
RETURN p
PROFILE and EXPLAIN
// Analyze query performance
PROFILE
MATCH (p:Person)-[:KNOWS*1..3]->(friend)
WHERE p.name = 'Alice'
RETURN friend.name
// Dry run without execution
EXPLAIN
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN p, friend
Limiting Results
// Limit results
MATCH (n:Person) RETURN n LIMIT 10
// Skip and limit (pagination)
MATCH (n:Person)
RETURN n
ORDER BY n.name
SKIP 20 LIMIT 10
Configuration Properties
Connection Properties
# Neo4j URI
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.uri=neo4j://localhost:7687
spring.neo4j.uri=neo4j+s://production.server:7687
# Authentication
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret
spring.neo4j.authentication.realm=native
spring.neo4j.authentication.kerberos-ticket=...
# Connection pool
spring.neo4j.pool.max-connection-pool-size=50
spring.neo4j.pool.idle-time-before-connection-test=PT30S
spring.neo4j.pool.max-connection-lifetime=PT1H
spring.neo4j.pool.connection-acquisition-timeout=PT60S
spring.neo4j.pool.metrics-enabled=true
Advanced Configuration
# Logging
spring.neo4j.logging.level=WARN
spring.neo4j.logging.log-leaked-sessions=true
# Connection timeout
spring.neo4j.connection-timeout=PT30S
# Max transaction retry time
spring.neo4j.max-transaction-retry-time=PT30S
# Encrypted connection
spring.neo4j.security.encrypted=true
spring.neo4j.security.trust-strategy=TRUST_ALL_CERTIFICATES
spring.neo4j.security.hostname-verification-enabled=true
Neo4j Driver Configuration Bean
@Configuration
public class Neo4jConfiguration {
@Bean
org.neo4j.driver.Config neo4jDriverConfig() {
return org.neo4j.driver.Config.builder()
.withMaxConnectionPoolSize(50)
.withConnectionAcquisitionTimeout(60, TimeUnit.SECONDS)
.withConnectionLivenessCheckTimeout(30, TimeUnit.SECONDS)
.withMaxConnectionLifetime(1, TimeUnit.HOURS)
.withLogging(Logging.slf4j())
.withEncryption()
.build();
}
@Bean
Configuration cypherDslConfiguration() {
return Configuration.newConfig()
.withDialect(Dialect.NEO4J_5)
.build();
}
}
Repository Methods
Query Derivation Keywords
| Keyword | Cypher Equivalent |
|---|---|
findBy |
MATCH ... RETURN |
existsBy |
MATCH ... RETURN count(*) > 0 |
countBy |
MATCH ... RETURN count(*) |
deleteBy |
MATCH ... DETACH DELETE |
And |
AND |
Or |
OR |
Between |
>= $lower AND <= $upper |
LessThan |
< |
LessThanEqual |
<= |
GreaterThan |
> |
GreaterThanEqual |
>= |
Before |
< (for dates) |
After |
> (for dates) |
IsNull |
IS NULL |
IsNotNull |
IS NOT NULL |
Like |
=~ '.*pattern.*' |
NotLike |
NOT =~ '.*pattern.*' |
StartingWith |
STARTS WITH |
EndingWith |
ENDS WITH |
Containing |
CONTAINS |
In |
IN |
NotIn |
NOT IN |
True |
= true |
False |
= false |
OrderBy...Asc |
ORDER BY ... ASC |
OrderBy...Desc |
ORDER BY ... DESC |
Method Return Types
| Return Type | Description |
|---|---|
Entity |
Single result or null |
Optional<Entity> |
Single result wrapped in Optional |
List<Entity> |
Multiple results |
Stream<Entity> |
Results as Java Stream |
Page<Entity> |
Paginated results |
Slice<Entity> |
Slice of results |
Mono<Entity> |
Reactive single result |
Flux<Entity> |
Reactive stream of results |
boolean |
Existence check |
long |
Count query |
Examples
public interface UserRepository extends Neo4jRepository<User, String> {
// Simple query derivation
Optional<User> findByEmail(String email);
List<User> findByAgeGreaterThan(Integer age);
List<User> findByAgeBetween(Integer minAge, Integer maxAge);
List<User> findByNameStartingWith(String prefix);
// Boolean queries
boolean existsByEmail(String email);
// Count queries
long countByAgeGreaterThan(Integer age);
// Delete queries
long deleteByAgeLessThan(Integer age);
// Sorting
List<User> findByAgeGreaterThanOrderByNameAsc(Integer age);
// Pagination
Page<User> findByAgeGreaterThan(Integer age, Pageable pageable);
// Stream
Stream<User> findByAgeBetween(Integer min, Integer max);
// Multiple conditions
List<User> findByNameAndAge(String name, Integer age);
List<User> findByNameOrEmail(String name, String email);
// Null checks
List<User> findByNicknameIsNull();
List<User> findByNicknameIsNotNull();
// Collection queries
List<User> findByRolesContaining(String role);
List<User> findByIdIn(Collection<String> ids);
}
Projections and DTOs
Interface-based Projections
// Closed projection - only declared properties
public interface UserSummary {
String getUsername();
String getEmail();
}
// Open projection - with SpEL
public interface UserWithFullName {
@Value("#{target.firstName + ' ' + target.lastName}")
String getFullName();
}
// Nested projection
public interface UserWithPosts {
String getUsername();
List<PostSummary> getPosts();
interface PostSummary {
String getTitle();
LocalDateTime getCreatedAt();
}
}
// Usage
public interface UserRepository extends Neo4jRepository<User, String> {
List<UserSummary> findAllBy();
Optional<UserWithFullName> findByUsername(String username);
}
Class-based DTOs
public record UserDTO(
String username,
String email,
LocalDateTime joinedAt
) {}
// Repository usage
public interface UserRepository extends Neo4jRepository<User, String> {
List<UserDTO> findAllBy();
}
Dynamic Projections
public interface UserRepository extends Neo4jRepository<User, String> {
<T> T findByUsername(String username, Class<T> type);
}
// Usage
UserSummary summary = repository.findByUsername("john", UserSummary.class);
UserDTO dto = repository.findByUsername("john", UserDTO.class);
User full = repository.findByUsername("john", User.class);
Transaction Management
Declarative Transactions
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public User createUser(CreateUserRequest request) {
User user = new User(request.username(), request.email());
return userRepository.save(user);
}
@Transactional(readOnly = true)
public Optional<User> getUser(String username) {
return userRepository.findByUsername(username);
}
@Transactional(
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.READ_COMMITTED,
timeout = 30
)
public void complexOperation() {
// Multiple repository calls in single transaction
// ...
}
}
Programmatic Transactions
@Service
public class TransactionalService {
private final Neo4jTransactionManager transactionManager;
public void executeInTransaction() {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.execute(status -> {
try {
// Your transactional code here
return someResult;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
}
}
Reactive Transactions
@Service
public class ReactiveUserService {
private final ReactiveUserRepository repository;
private final ReactiveNeo4jTransactionManager transactionManager;
public Mono<User> createUser(CreateUserRequest request) {
return transactionManager.getReactiveTransaction()
.flatMap(status -> {
User user = new User(request.username(), request.email());
return repository.save(user)
.doOnError(e -> status.setRollbackOnly());
});
}
}
Performance Tuning
Index Creation
Create indexes on frequently queried properties:
// Single property index
CREATE INDEX user_email FOR (u:User) ON (u.email);
// Composite index
CREATE INDEX user_name_age FOR (u:User) ON (u.name, u.age);
// Full-text index
CREATE FULLTEXT INDEX user_search FOR (u:User) ON EACH [u.name, u.bio];
// Show indexes
SHOW INDEXES;
// Drop index
DROP INDEX user_email;
Query Optimization Tips
-
Use specific labels:
// Good MATCH (u:User {email: $email}) RETURN u // Bad MATCH (n {email: $email}) RETURN n -
Filter early:
// Good MATCH (u:User) WHERE u.age > 18 MATCH (u)-[:POSTED]->(p:Post) RETURN p // Bad MATCH (u:User)-[:POSTED]->(p:Post) WHERE u.age > 18 RETURN p -
Use projections to fetch only needed data:
// Good List<UserSummary> findAllBy(); // Bad (when you only need summary) List<User> findAll(); -
Limit result sets:
// Use pagination Page<User> findAll(Pageable pageable); // Or explicit limits @Query("MATCH (u:User) RETURN u LIMIT $limit") List<User> findTopUsers(@Param("limit") int limit);
Connection Pooling
@Bean
org.neo4j.driver.Config driverConfig() {
return org.neo4j.driver.Config.builder()
.withMaxConnectionPoolSize(50)
.withConnectionAcquisitionTimeout(60, TimeUnit.SECONDS)
.withIdleTimeBeforeConnectionTest(30, TimeUnit.SECONDS)
.build();
}
Batch Operations
// Save in batches
@Service
public class BatchService {
private final UserRepository repository;
public void saveUsersInBatches(List<User> users) {
int batchSize = 1000;
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
List<User> batch = users.subList(i, end);
repository.saveAll(batch);
}
}
}
Monitoring and Metrics
# Enable driver metrics
spring.neo4j.pool.metrics-enabled=true
# Log slow queries (if using Neo4j Enterprise)
# Set in neo4j.conf:
# dbms.logs.query.enabled=true
# dbms.logs.query.threshold=1s