--- name: neo4j-spring-data-skill description: Use when building Spring Boot applications with Neo4j using Spring Data Neo4j (SDN 7.x/8.x): @Node entity mapping, @Relationship, @RelationshipProperties, Neo4jRepository, ReactiveNeo4jRepository, @Query annotations, application.yml configuration, projections, Neo4jClient, Neo4jTemplate, transactions, auditing, or Spring AI Neo4jVectorStore vector search. Does NOT handle raw Java driver code without Spring — use neo4j-driver-java-skill. Does NOT handle Cypher query authoring — use neo4j-cypher-skill. Does NOT handle driver version upgrades — use neo4j-migration-skill. version: 1.0.1 allowed-tools: Bash WebFetch --- # Neo4j Spring Data Skill ## When to Use - Configuring Spring Boot with Neo4j (`spring-boot-starter-data-neo4j`) - Writing `@Node` entity classes and `@Relationship`/`@RelationshipProperties` mappings - Defining `Neo4jRepository` or `ReactiveNeo4jRepository` interfaces - Writing `@Query` annotations with Cypher on repository methods - Using Spring projections (interface-based, DTO, dynamic) with Neo4j - Configuring `application.yml` for Neo4j connection - Custom queries via `Neo4jClient` or `Neo4jTemplate` - Spring AI `Neo4jVectorStore` for vector search in Spring apps - Transaction management, auditing, optimistic locking ## When NOT to Use - **Raw Java driver without Spring** → `neo4j-driver-java-skill` - **Cypher query authoring** → `neo4j-cypher-skill` - **Driver version upgrades** → `neo4j-migration-skill` - **GDS algorithms** → `neo4j-gds-skill` --- ## Version Matrix | SDN | Spring Boot | Spring Framework | Java | Neo4j | |--------|-------------|-----------------|------|-------| | 8.0.x | 3.3.x / 3.4.x | 6.2.x | 17+ | 5.15+ | | 8.1.x | 3.4.x+ | 7.0.x | 17+ | 5.15+ | | 7.5.x | 3.2.x | 6.1.x | 17+ | 4.4+ | Use `spring-boot-starter-data-neo4j` — it pulls SDN + driver. No explicit SDN version needed when using Spring Boot BOM. --- ## Setup ### Maven ```xml org.springframework.boot spring-boot-starter-data-neo4j ``` ### Gradle ```gradle implementation 'org.springframework.boot:spring-boot-starter-data-neo4j' ``` ### Reactive stack (add alongside above) ```xml org.springframework.boot spring-boot-starter-webflux ``` --- ## Configuration ### application.yml — imperative (standard) ```yaml spring: neo4j: uri: ${NEO4J_URI:bolt://localhost:7687} authentication: username: ${NEO4J_USERNAME:neo4j} password: ${NEO4J_PASSWORD} data: neo4j: database: ${NEO4J_DATABASE:neo4j} ``` ### application.yml — Aura (TLS required) ```yaml spring: neo4j: uri: ${NEO4J_URI} # neo4j+s://xxxx.databases.neo4j.io authentication: username: ${NEO4J_USERNAME:neo4j} password: ${NEO4J_PASSWORD} data: neo4j: database: ${NEO4J_DATABASE:neo4j} ``` Credentials: store in `.env`; never hardcode. Verify `.env` is in `.gitignore`. --- ## Entity Mapping ```java import org.springframework.data.neo4j.core.schema.*; // Internal generated ID (default for most cases) @Node("Person") public class PersonEntity { @Id @GeneratedValue private Long id; // element ID (Long) private String name; @Property("birth_year") private Integer birthYear; // custom property name @Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING) private List friends = new ArrayList<>(); } // UUID business key @Node("Product") public class ProductEntity { @Id @GeneratedValue(generatorClass = GeneratedValue.UUIDStringGenerator.class) private String id; @Version private Long version; // optimistic locking; required with business key } // User-assigned key (caller sets value; no @GeneratedValue) @Node("Country") public class CountryEntity { @Id private String isoCode; private String name; } // Multiple static labels @Node(primaryLabel = "Vehicle", labels = {"Car", "Auditable"}) public class CarEntity { ... } // Runtime labels @Node("Content") public class ContentEntity { @Id @GeneratedValue private Long id; @DynamicLabels private Set tags = new HashSet<>(); // labels added at runtime } ``` --- ## Relationship Properties Use `@RelationshipProperties` when the relationship itself carries data. ```java @RelationshipProperties public class RolesRelationship { @RelationshipId // internal relationship ID; required private Long id; private List roles; @TargetNode // marks the other end of the relationship private PersonEntity person; } ``` ```java @Node("Movie") public class MovieEntity { @Id @GeneratedValue private Long id; private String title; @Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING) private List actorsAndRoles = new ArrayList<>(); } ``` --- ## Repository Interfaces ### Basic CRUD ```java import org.springframework.data.neo4j.repository.Neo4jRepository; public interface PersonRepository extends Neo4jRepository { Optional findByName(String name); List findByBirthYearBetween(int from, int to); List findByNameContainingIgnoreCase(String fragment); long countByBirthYearGreaterThan(int year); void deleteByName(String name); } ``` ### @Query — custom Cypher ```java // CORRECT: $param bound parameter @Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f:Person) RETURN f") List findFriendsOf(String name); // With pagination @Query(value = "MATCH (p:Person) RETURN p ORDER BY p.name", countQuery = "MATCH (p:Person) RETURN count(p)") Page findAllPaged(Pageable pageable); // Return relationship-rich entity; map target via @Node return @Query("MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person {name: $name}) RETURN m, collect(r), collect(p)") List findMoviesActedInBy(String name); ``` **Security rule**: NEVER string-concatenate user input into Cypher. Always use `$paramName`. ### Pagination and sorting ```java Page findByBirthYearGreaterThan(int year, Pageable pageable); List findTop10ByOrderByNameAsc(); List findByName(String name, Sort sort); ``` Usage: ```java Pageable page = PageRequest.of(0, 20, Sort.by("name").ascending()); Page result = repo.findByBirthYearGreaterThan(1980, page); ``` --- ## Projections ### Interface projection (closed — query-optimizable) ```java public interface PersonSummary { String getName(); Integer getBirthYear(); } List findByBirthYearLessThan(int year); ``` ### DTO projection (record — preferred in Java 17+) ```java public record PersonDto(String name, Integer birthYear) {} List findByName(String name); ``` ### Dynamic projection ```java List findByName(String name, Class type); // Usage repo.findByName("Alice", PersonSummary.class); repo.findByName("Alice", PersonEntity.class); ``` ### Open projection — SpEL (disables query optimization) ```java public interface FullName { @Value("#{target.name + ' (' + target.birthYear + ')'}") String getDisplayName(); } ``` --- ## Reactive Repository ```java import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public interface ReactivePersonRepository extends ReactiveNeo4jRepository { Mono findByName(String name); @Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f") Flux findFriendsOf(String name); } ``` Do NOT mix imperative and reactive database access in the same application context. --- ## Custom Repository Implementation Fragment pattern — use when `@Query` is not enough. ```java // 1. Fragment interface public interface PersonRepositoryCustom { List findByComplexCriteria(String criteria); } // 2. Impl — must end with "Impl" public class PersonRepositoryCustomImpl implements PersonRepositoryCustom { private final Neo4jClient neo4jClient; PersonRepositoryCustomImpl(Neo4jClient c) { this.neo4jClient = c; } @Override public List findByComplexCriteria(String c) { return new ArrayList<>(neo4jClient .query("MATCH (p:Person) WHERE p.name CONTAINS $c RETURN p").bind(c).to("c") .fetchAs(PersonEntity.class) .mappedBy((t, r) -> { var e = new PersonEntity(); e.setName(r.get("p").asNode().get("name").asString()); return e; }) .all()); } } // 3. Compose public interface PersonRepository extends Neo4jRepository, PersonRepositoryCustom {} ``` --- ## Neo4jClient — Low-Level Queries Use when `@Query` is insufficient or you need full control over Cypher execution. ```java // Bind params + fetch single scalar neo4jClient.query("MATCH (p:Person {name: $name}) RETURN count(*) AS cnt") .bind("Alice").to("name") .fetchAs(Long.class) .mappedBy((t, r) -> r.get("cnt").asLong()) .one(); // Bind + run write (no result) neo4jClient.query("MERGE (p:Person {name: $name})") .bind(personName).to("name") .run(); // Custom object mapping neo4jClient.query("MATCH (p:Person)-[:DIRECTED]->(m:Movie) WHERE p.name=$n RETURN p, collect(m) AS movies") .bind("Lilly Wachowski").to("n") .fetchAs(Director.class) .mappedBy((typeSystem, record) -> new Director( record.get("p").asNode().get("name").asString(), record.get("movies").asList(v -> new Movie(v.get("title").asString())) )).one(); ``` Full API: [references/neo4j-client.md](references/neo4j-client.md) --- ## Transaction Management ```java @Service @Transactional // class-level: all methods transactional public class PersonService { @Transactional(readOnly = true) // read-only hint public Optional findByName(String name) { ... } @Transactional // explicit write public PersonEntity save(PersonEntity p) { return repository.save(p); } } ``` `Neo4jTransactionManager` auto-configured. Do NOT mix with JPA `PlatformTransactionManager` without explicit qualifier. Use `@Transactional` on concrete class, not interface. --- ## Spring AI — Neo4jVectorStore ### Dependency ```xml org.springframework.ai spring-ai-starter-vector-store-neo4j ``` ### application.yml ```yaml spring: ai: vectorstore: neo4j: initialize-schema: true # creates vector index on first run index-name: my-index embedding-dimension: 1536 # must match your embedding model distance-type: cosine # cosine (default) or euclidean label: Document # node label for stored chunks embedding-property: embedding # property for the vector ``` Requires Neo4j 5.15+. Reuses `spring.neo4j.*` connection config. ### Usage ```java @Autowired VectorStore vectorStore; // Store vectorStore.add(List.of(new Document("text", Map.of("author", "alice")))); // Similarity search List results = vectorStore.similaritySearch( SearchRequest.builder().query("spring neo4j").topK(5).similarityThreshold(0.75).build() ); // With metadata filter vectorStore.similaritySearch( SearchRequest.builder().query("spring neo4j").topK(5) .filterExpression("author == 'alice'").build() ); ``` --- ## Common Errors | Error | Cause | Fix | |---|---|---| | `MappingException: Could not find entity` | Entity not scanned | Check `@EnableNeo4jRepositories` base package | | Relationships null after load | Default depth may skip deep rels | Use `@Query` with `RETURN m, collect(r), collect(p)` | | N+1 queries | Per-entity relationship fetch | Rewrite with single `@Query`; use projections | | `OptimisticLockingFailureException` | Stale `@Version` on concurrent write | Retry in service layer | | `IllegalStateException: Cannot mix reactive/imperative` | Both repo types in same context | Pick one stack | | Projection null fields | Getter name mismatch | Match getter to property name; check `@Property` alias | | `@Query` empty with rels | Missing `collect(r), collect(p)` | Return root node + rels + related nodes together | | `Cannot delete node, node has relationships` | `deleteById` without detach | Use `@Query` with `DETACH DELETE` | | Transaction not rolling back | `@Transactional` on interface | Apply on concrete service class | --- ## Relationship Loading — Key Rule SDN loads related entities eagerly up to a configured depth (default: 1 hop). For deeper graphs: ```java // Explicit @Query to control what gets loaded @Query(""" MATCH (m:Movie)<-[r:ACTED_IN]-(p:Person) WHERE m.title = $title RETURN m, collect(r), collect(p) """) Optional findByTitleWithCast(String title); ``` `collect(r), collect(p)` in RETURN is required for SDN to map `@RelationshipProperties` correctly. --- ## References - [Spring Data Neo4j Reference (8.x)](https://docs.spring.io/spring-data/neo4j/reference/) - [Spring AI Neo4jVectorStore](https://docs.spring.io/spring-ai/reference/api/vectordbs/neo4j.html) - [GraphAcademy: Building Neo4j Apps with Spring Data](https://graphacademy.neo4j.com/courses/app-spring-data/) - [Neo4j Getting Started — SDN](https://neo4j.com/docs/getting-started/languages-guides/java/spring-data-neo4j/) - [SDN Advanced projections](https://docs.spring.io/spring-data/neo4j/reference/repositories/projections.html) - [SDN Auditing](https://docs.spring.io/spring-data/neo4j/reference/object-mapping/auditing.html) - [Modeling pitfalls, projection guide, type mapping](references/modeling-pitfalls.md) --- ## Checklist - [ ] `@Node` uses explicit label string, not default class name - [ ] `@Id @GeneratedValue` (or `@Id` + `@Version` for business key with optimistic lock) - [ ] `@RelationshipProperties` class has `@RelationshipId` and `@TargetNode` - [ ] `@Relationship` direction is explicit (OUTGOING / INCOMING) - [ ] `@Query` Cypher uses `$paramName` — no string concatenation - [ ] Relationship-rich `@Query` returns `collect(r), collect(p)` alongside root node - [ ] Database name set in `application.yml` (avoids default DB ambiguity) - [ ] Unique constraint exists in DB for any business key used in repository lookups - [ ] `@Transactional` on concrete service class (not interface) - [ ] No imperative + reactive mix in same application context - [ ] Credentials in env vars; `.env` in `.gitignore` - [ ] `spring.ai.vectorstore.neo4j.initialize-schema: true` for first run (Spring AI)