Initial commit
This commit is contained in:
307
skills/spring-data-neo4j/SKILL.md
Normal file
307
skills/spring-data-neo4j/SKILL.md
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
name: spring-data-neo4j
|
||||
description: Expert in Spring Data Neo4j integration patterns for graph database development. Use when working with Neo4j graph databases, node entities, relationships, Cypher queries, reactive Neo4j operations, or Spring Data Neo4j repositories. Essential for graph data modeling, relationship mapping, custom queries, and Neo4j testing strategies.
|
||||
version: 1.1.0
|
||||
allowed-tools: Read, Write, Bash, Grep, Glob
|
||||
category: backend
|
||||
tags: [spring-data, neo4j, graph-database, database, java, spring-boot]
|
||||
---
|
||||
|
||||
# Spring Data Neo4j Integration Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
To use this skill when you need to:
|
||||
- Set up Spring Data Neo4j in a Spring Boot application
|
||||
- Create and map graph node entities and relationships
|
||||
- Implement Neo4j repositories with custom queries
|
||||
- Write Cypher queries using @Query annotations
|
||||
- Configure Neo4j connections and dialects
|
||||
- Test Neo4j repositories with embedded databases
|
||||
- Work with both imperative and reactive Neo4j operations
|
||||
- Map complex graph relationships with bidirectional or unidirectional directions
|
||||
- Use Neo4j's internal ID generation or custom business keys
|
||||
|
||||
## Overview
|
||||
|
||||
Spring Data Neo4j provides three levels of abstraction for Neo4j integration:
|
||||
- **Neo4j Client**: Low-level abstraction for direct database access
|
||||
- **Neo4j Template**: Medium-level template-based operations
|
||||
- **Neo4j Repositories**: High-level repository pattern with query derivation
|
||||
|
||||
Key features include reactive and imperative operation modes, immutable entity mapping, custom query support via @Query annotation, Spring's Conversion Service integration, and full support for graph relationships and traversals.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### Dependencies
|
||||
|
||||
**Maven:**
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-neo4j</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Gradle:**
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
**application.properties:**
|
||||
```properties
|
||||
spring.neo4j.uri=bolt://localhost:7687
|
||||
spring.neo4j.authentication.username=neo4j
|
||||
spring.neo4j.authentication.password=secret
|
||||
```
|
||||
|
||||
**Configure Neo4j Cypher-DSL Dialect:**
|
||||
```java
|
||||
@Configuration
|
||||
public class Neo4jConfig {
|
||||
|
||||
@Bean
|
||||
Configuration cypherDslConfiguration() {
|
||||
return Configuration.newConfig()
|
||||
.withDialect(Dialect.NEO4J_5).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Entity Mapping
|
||||
|
||||
### Node Entity with Business Key
|
||||
|
||||
```java
|
||||
@Node("Movie")
|
||||
public class MovieEntity {
|
||||
|
||||
@Id
|
||||
private final String title; // Business key as ID
|
||||
|
||||
@Property("tagline")
|
||||
private final String description;
|
||||
|
||||
private final Integer year;
|
||||
|
||||
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING)
|
||||
private List<Roles> actorsAndRoles = new ArrayList<>();
|
||||
|
||||
@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
|
||||
private List<PersonEntity> directors = new ArrayList<>();
|
||||
|
||||
public MovieEntity(String title, String description, Integer year) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.year = year;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Node Entity with Generated ID
|
||||
|
||||
```java
|
||||
@Node("Movie")
|
||||
public class MovieEntity {
|
||||
|
||||
@Id @GeneratedValue
|
||||
private Long id;
|
||||
|
||||
private final String title;
|
||||
|
||||
@Property("tagline")
|
||||
private final String description;
|
||||
|
||||
public MovieEntity(String title, String description) {
|
||||
this.id = null; // Never set manually
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
// Wither method for immutability with generated IDs
|
||||
public MovieEntity withId(Long id) {
|
||||
if (this.id != null && this.id.equals(id)) {
|
||||
return this;
|
||||
} else {
|
||||
MovieEntity newObject = new MovieEntity(this.title, this.description);
|
||||
newObject.id = id;
|
||||
return newObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Patterns
|
||||
|
||||
### Basic Repository Interface
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public interface MovieRepository extends Neo4jRepository<MovieEntity, String> {
|
||||
|
||||
// Query derivation from method name
|
||||
MovieEntity findOneByTitle(String title);
|
||||
|
||||
List<MovieEntity> findAllByYear(Integer year);
|
||||
|
||||
List<MovieEntity> findByYearBetween(Integer startYear, Integer endYear);
|
||||
}
|
||||
```
|
||||
|
||||
### Reactive Repository
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public interface MovieRepository extends ReactiveNeo4jRepository<MovieEntity, String> {
|
||||
|
||||
Mono<MovieEntity> findOneByTitle(String title);
|
||||
|
||||
Flux<MovieEntity> findAllByYear(Integer year);
|
||||
}
|
||||
```
|
||||
|
||||
**Imperative vs Reactive:**
|
||||
- Use `Neo4jRepository` for blocking, imperative operations
|
||||
- Use `ReactiveNeo4jRepository` for non-blocking, reactive operations
|
||||
- **Do not mix imperative and reactive in the same application**
|
||||
- Reactive requires Neo4j 4+ on the database side
|
||||
|
||||
## Custom Queries with @Query
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public interface AuthorRepository extends Neo4jRepository<Author, Long> {
|
||||
|
||||
@Query("MATCH (b:Book)-[:WRITTEN_BY]->(a:Author) " +
|
||||
"WHERE a.name = $name AND b.year > $year " +
|
||||
"RETURN b")
|
||||
List<Book> findBooksAfterYear(@Param("name") String name,
|
||||
@Param("year") Integer year);
|
||||
|
||||
@Query("MATCH (b:Book)-[:WRITTEN_BY]->(a:Author) " +
|
||||
"WHERE a.name = $name " +
|
||||
"RETURN b ORDER BY b.year DESC")
|
||||
List<Book> findBooksByAuthorOrderByYearDesc(@Param("name") String name);
|
||||
}
|
||||
```
|
||||
|
||||
**Custom Query Best Practices:**
|
||||
- Use `$parameterName` for parameter placeholders
|
||||
- Use `@Param` annotation when parameter name differs from method parameter
|
||||
- MATCH specifies node patterns and relationships
|
||||
- WHERE filters results
|
||||
- RETURN defines what to return
|
||||
|
||||
## Testing Strategies
|
||||
|
||||
### Neo4j Harness for Integration Testing
|
||||
|
||||
**Test Configuration:**
|
||||
```java
|
||||
@DataNeo4jTest
|
||||
class BookRepositoryIntegrationTest {
|
||||
|
||||
private static Neo4j embeddedServer;
|
||||
|
||||
@BeforeAll
|
||||
static void initializeNeo4j() {
|
||||
embeddedServer = Neo4jBuilders.newInProcessBuilder()
|
||||
.withDisabledServer() // No HTTP access needed
|
||||
.withFixture(
|
||||
"CREATE (b:Book {isbn: '978-0547928210', " +
|
||||
"name: 'The Fellowship of the Ring', year: 1954})" +
|
||||
"-[:WRITTEN_BY]->(a:Author {id: 1, name: 'J. R. R. Tolkien'}) " +
|
||||
"CREATE (b2:Book {isbn: '978-0547928203', " +
|
||||
"name: 'The Two Towers', year: 1956})" +
|
||||
"-[:WRITTEN_BY]->(a)"
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopNeo4j() {
|
||||
embeddedServer.close();
|
||||
}
|
||||
|
||||
@DynamicPropertySource
|
||||
static void neo4jProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.neo4j.uri", embeddedServer::boltURI);
|
||||
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
|
||||
registry.add("spring.neo4j.authentication.password", () -> "null");
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private BookRepository bookRepository;
|
||||
|
||||
@Test
|
||||
void givenBookExists_whenFindOneByTitle_thenBookIsReturned() {
|
||||
Book book = bookRepository.findOneByTitle("The Fellowship of the Ring");
|
||||
assertThat(book.getIsbn()).isEqualTo("978-0547928210");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Progress from basic to advanced examples covering complete movie database, social network patterns, e-commerce product catalogs, custom queries, and reactive operations.
|
||||
|
||||
See [examples](./references/examples.md) for comprehensive code examples.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Entity Design
|
||||
- Use immutable entities with final fields
|
||||
- Choose between business keys (@Id) or generated IDs (@Id @GeneratedValue)
|
||||
- Keep entities focused on graph structure, not business logic
|
||||
- Use proper relationship directions (INCOMING, OUTGOING, UNDIRECTED)
|
||||
|
||||
### Repository Design
|
||||
- Extend `Neo4jRepository` for imperative or `ReactiveNeo4jRepository` for reactive
|
||||
- Use query derivation for simple queries
|
||||
- Write custom @Query for complex graph patterns
|
||||
- Don't mix imperative and reactive in same application
|
||||
|
||||
### Configuration
|
||||
- Always configure Cypher-DSL dialect explicitly
|
||||
- Use environment-specific properties for credentials
|
||||
- Never hardcode credentials in source code
|
||||
- Configure connection pooling based on load
|
||||
|
||||
### Testing
|
||||
- Use Neo4j Harness for integration tests
|
||||
- Provide test data via `withFixture()` Cypher queries
|
||||
- Use `@DataNeo4jTest` for test slicing
|
||||
- Test both successful and edge-case scenarios
|
||||
|
||||
### Architecture
|
||||
- Use constructor injection exclusively
|
||||
- Separate domain entities from DTOs
|
||||
- Follow feature-based package structure
|
||||
- Keep domain layer framework-agnostic
|
||||
|
||||
### Security
|
||||
- Use Spring Boot property overrides for credentials
|
||||
- Configure proper authentication and authorization
|
||||
- Validate input parameters in service layer
|
||||
- Use parameterized queries to prevent Cypher injection
|
||||
|
||||
## References
|
||||
|
||||
For detailed documentation including complete API reference, Cypher query patterns, and configuration options:
|
||||
|
||||
- [Annotations Reference](./references/reference.md#annotations-reference)
|
||||
- [Cypher Query Language](./references/reference.md#cypher-query-language)
|
||||
- [Configuration Properties](./references/reference.md#configuration-properties)
|
||||
- [Repository Methods](./references/reference.md#repository-methods)
|
||||
- [Projections and DTOs](./references/reference.md#projections-and-dtos)
|
||||
- [Transaction Management](./references/reference.md#transaction-management)
|
||||
- [Performance Tuning](./references/reference.md#performance-tuning)
|
||||
|
||||
### External Resources
|
||||
- [Spring Data Neo4j Official Documentation](https://docs.spring.io/spring-data/neo4j/reference/)
|
||||
- [Neo4j Developer Guide](https://neo4j.com/developer/)
|
||||
- [Spring Data Commons Documentation](https://docs.spring.io/spring-data/commons/reference/)
|
||||
1005
skills/spring-data-neo4j/references/examples.md
Normal file
1005
skills/spring-data-neo4j/references/examples.md
Normal file
File diff suppressed because it is too large
Load Diff
860
skills/spring-data-neo4j/references/reference.md
Normal file
860
skills/spring-data-neo4j/references/reference.md
Normal file
@@ -0,0 +1,860 @@
|
||||
# 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
|
||||
|
||||
1. [Annotations Reference](#annotations-reference)
|
||||
2. [Cypher Query Language](#cypher-query-language)
|
||||
3. [Configuration Properties](#configuration-properties)
|
||||
4. [Repository Methods](#repository-methods)
|
||||
5. [Projections and DTOs](#projections-and-dtos)
|
||||
6. [Transaction Management](#transaction-management)
|
||||
7. [Performance Tuning](#performance-tuning)
|
||||
|
||||
## Annotations Reference
|
||||
|
||||
### Entity Annotations
|
||||
|
||||
#### @Node
|
||||
|
||||
Marks a class as a Neo4j node entity.
|
||||
|
||||
```java
|
||||
@Node // Label defaults to class name
|
||||
@Node("CustomLabel") // Explicit label
|
||||
@Node({"Label1", "Label2"}) // Multiple labels
|
||||
public class MyEntity {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `value` or `labels`: String or String array for node labels
|
||||
- `primaryLabel`: Specify which label is primary (when using multiple labels)
|
||||
|
||||
#### @Id
|
||||
|
||||
Marks a field as the entity identifier.
|
||||
|
||||
```java
|
||||
@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.
|
||||
|
||||
```java
|
||||
@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 ID
|
||||
- `UUIDStringGenerator`: Generates UUID strings
|
||||
|
||||
#### @Property
|
||||
|
||||
Maps a field to a different property name in Neo4j.
|
||||
|
||||
```java
|
||||
@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.
|
||||
|
||||
```java
|
||||
@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 Neo4j
|
||||
- `direction`: OUTGOING, INCOMING, or UNDIRECTED
|
||||
- Default direction is OUTGOING if not specified
|
||||
|
||||
**Direction Guidelines:**
|
||||
- `OUTGOING`: This node → target node
|
||||
- `INCOMING`: Target node → this node
|
||||
- `UNDIRECTED`: Ignores direction when querying
|
||||
|
||||
#### @RelationshipProperties
|
||||
|
||||
Marks a class as relationship properties container.
|
||||
|
||||
```java
|
||||
@RelationshipProperties
|
||||
public class ActedIn {
|
||||
|
||||
@Id @GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@TargetNode
|
||||
private Movie movie;
|
||||
|
||||
private List<String> roles;
|
||||
private Integer screenTime;
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `@Id` field (can be generated)
|
||||
- `@TargetNode` field pointing to target entity
|
||||
|
||||
### Repository Annotations
|
||||
|
||||
#### @Query
|
||||
|
||||
Defines custom Cypher query for a repository method.
|
||||
|
||||
```java
|
||||
@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 `$paramName` for named parameters with `@Param`
|
||||
- Use `$0`, `$1`, etc. for positional parameters
|
||||
- SpEL expressions supported: `#{#entityName}`
|
||||
|
||||
#### @Param
|
||||
|
||||
Binds method parameter to query parameter.
|
||||
|
||||
```java
|
||||
@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.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableNeo4jRepositories(basePackages = "com.example.repositories")
|
||||
public class Neo4jConfiguration {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `basePackages`: Packages to scan for repositories
|
||||
- `basePackageClasses`: Type-safe package specification
|
||||
- `repositoryImplementationPostfix`: Custom implementation suffix (default: "Impl")
|
||||
|
||||
**Note:** Auto-enabled by Spring Boot starter, manual configuration rarely needed.
|
||||
|
||||
#### @DataNeo4jTest
|
||||
|
||||
Test slice annotation for Neo4j tests.
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
```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
|
||||
|
||||
```properties
|
||||
# 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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
// 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
|
||||
|
||||
```java
|
||||
public record UserDTO(
|
||||
String username,
|
||||
String email,
|
||||
LocalDateTime joinedAt
|
||||
) {}
|
||||
|
||||
// Repository usage
|
||||
public interface UserRepository extends Neo4jRepository<User, String> {
|
||||
List<UserDTO> findAllBy();
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Projections
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
@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:
|
||||
|
||||
```cypher
|
||||
// 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
|
||||
|
||||
1. **Use specific labels:**
|
||||
```cypher
|
||||
// Good
|
||||
MATCH (u:User {email: $email}) RETURN u
|
||||
|
||||
// Bad
|
||||
MATCH (n {email: $email}) RETURN n
|
||||
```
|
||||
|
||||
2. **Filter early:**
|
||||
```cypher
|
||||
// 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
|
||||
```
|
||||
|
||||
3. **Use projections to fetch only needed data:**
|
||||
```java
|
||||
// Good
|
||||
List<UserSummary> findAllBy();
|
||||
|
||||
// Bad (when you only need summary)
|
||||
List<User> findAll();
|
||||
```
|
||||
|
||||
4. **Limit result sets:**
|
||||
```java
|
||||
// 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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
// 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
|
||||
|
||||
```properties
|
||||
# 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
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Neo4j Cypher Manual](https://neo4j.com/docs/cypher-manual/current/)
|
||||
- [Spring Data Neo4j Reference](https://docs.spring.io/spring-data/neo4j/reference/)
|
||||
- [Neo4j Java Driver Documentation](https://neo4j.com/docs/java-manual/current/)
|
||||
- [Graph Data Modeling Guide](https://neo4j.com/developer/data-modeling/)
|
||||
Reference in New Issue
Block a user