neo4j-spring-data-skill について
Spring Data Neo4j を使う Spring アプリ開発者向けのスキルです。エンティティマッピング、リポジトリ、トランザクション、リアクティブアクセスに加え、Spring AI の Neo4jVectorStore も扱います。
主な機能
- 01Spring Boot と Neo4j の設定
- 02ノードとリレーションシップのマッピング
- 03リポジトリクエリとプロジェクション
- 04リアクティブリポジトリのパターン
- 05トランザクション管理の指針
- 06Neo4j ベクトル検索の設定
活用シーン
- 01Spring アプリの Neo4j 接続設定
- 02グラフエンティティと関係のマッピング
- 03ページング対応の Cypher リポジトリクエリ作成
- 04Spring AI で Neo4j ベクトル検索を利用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ neo4j-contrib/ neo4j-skills --skill neo4j-spring-data-skillインストール後、Claude Code で/neo4j-spring-data-skillを入力して読み込まれたか確認します。
同梱ファイルREADME.mdreferences/modeling-pitfalls.md
詳細
Neo4j Spring Data スキル
使用する場面
- Neo4j で Spring Boot(
spring-boot-starter-data-neo4j)を設定する @Nodeエンティティクラスを作成し、@Relationship/@RelationshipPropertiesのマッピングを定義するNeo4jRepositoryまたはReactiveNeo4jRepositoryインターフェースを定義する- リポジトリメソッドに Cypher を使った
@Queryアノテーションを記述する - Neo4j で Spring のプロジェクション(インターフェースベース、DTO、動的)を使用する
- Neo4j 接続用に
application.ymlを設定する Neo4jClientまたはNeo4jTemplateを使ってカスタムクエリを作成する- Spring アプリで Spring AI の
Neo4jVectorStoreをベクトル検索に使用する - トランザクション管理、監査、楽観的ロックを行う
使用しない場面
- Spring を使わずに生の Java ドライバーを使用する →
neo4j-driver-java-skill - Cypher クエリを作成する →
neo4j-cypher-skill - ドライバーのバージョンをアップグレードする →
neo4j-migration-skill - GDS アルゴリズムを使用する →
neo4j-gds-skill
バージョンマトリックス
| 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+ |
spring-boot-starter-data-neo4j を使用してください。SDN とドライバーが取り込まれます。Spring Boot BOM を使用する場合、SDN のバージョンを明示的に指定する必要はありません。
セットアップ
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
Gradle
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
リアクティブスタック(上記に追加)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
設定
application.yml — 命令型(標準)
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 必須)
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}
認証情報は .env に保存し、ハードコードしないでください。.env が .gitignore に含まれていることを確認してください。
エンティティのマッピング
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<PersonEntity> 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<String> tags = new HashSet<>(); // labels added at runtime
}
リレーションシップのプロパティ
リレーションシップ自体がデータを持つ場合は、@RelationshipProperties を使用してください。
@RelationshipProperties
public class RolesRelationship {
@RelationshipId // internal relationship ID; required
private Long id;
private List<String> roles;
@TargetNode // marks the other end of the relationship
private PersonEntity person;
}
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private String title;
@Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
private List<RolesRelationship> actorsAndRoles = new ArrayList<>();
}
リポジトリインターフェース
基本的な CRUD
import org.springframework.data.neo4j.repository.Neo4jRepository;
public interface PersonRepository extends Neo4jRepository<PersonEntity, Long> {
Optional<PersonEntity> findByName(String name);
List<PersonEntity> findByBirthYearBetween(int from, int to);
List<PersonEntity> findByNameContainingIgnoreCase(String fragment);
long countByBirthYearGreaterThan(int year);
void deleteByName(String name);
}
@Query — カスタム Cypher
// CORRECT: $param bound parameter
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f:Person) RETURN f")
List<PersonEntity> findFriendsOf(String name);
// With pagination
@Query(value = "MATCH (p:Person) RETURN p ORDER BY p.name",
countQuery = "MATCH (p:Person) RETURN count(p)")
Page<PersonEntity> 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<MovieEntity> findMoviesActedInBy(String name);
セキュリティルール:ユーザー入力を Cypher に文字列連結で埋め込むことは絶対にしないでください。必ず $paramName を使用してください。
ページネーションとソート
Page<PersonEntity> findByBirthYearGreaterThan(int year, Pageable pageable);
List<PersonEntity> findTop10ByOrderByNameAsc();
List<PersonEntity> findByName(String name, Sort sort);
使用例:
Pageable page = PageRequest.of(0, 20, Sort.by("name").ascending());
Page<PersonEntity> result = repo.findByBirthYearGreaterThan(1980, page);
プロジェクション
インターフェースプロジェクション(クローズド — クエリを最適化可能)
public interface PersonSummary {
String getName();
Integer getBirthYear();
}
List<PersonSummary> findByBirthYearLessThan(int year);
DTO プロジェクション(record — Java 17 以降では推奨)
public record PersonDto(String name, Integer birthYear) {}
List<PersonDto> findByName(String name);
動的プロジェクション
<T> List<T> findByName(String name, Class<T> type);
// Usage
repo.findByName("Alice", PersonSummary.class);
repo.findByName("Alice", PersonEntity.class);
オープンプロジェクション — SpEL(クエリ最適化が無効になります)
public interface FullName {
@Value("#{target.name + ' (' + target.birthYear + ')'}") String getDisplayName();
}
リアクティブリポジトリ
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ReactivePersonRepository extends ReactiveNeo4jRepository<PersonEntity, Long> {
Mono<PersonEntity> findByName(String name);
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f")
Flux<PersonEntity> findFriendsOf(String name);
}
同じアプリケーションコンテキスト内で、命令型とリアクティブ型のデータベースアクセスを混在させないでください。
カスタムリポジトリの実装
フラグメントパターン — @Query では不十分な場合に使用します。
// 1. Fragment interface
public interface PersonRepositoryCustom {
List<PersonEntity> 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<PersonEntity> 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<PersonEntity, Long>, PersonRepositoryCustom {}
Neo4jClient — 低レベルクエリ
@Query では不十分な場合や、Cypher の実行を完全に制御する必要がある場合に使用します。
// 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();
API の詳細:references/neo4j-client.md
トランザクション管理
@Service
@Transactional // class-level: all methods transactional
public class PersonService {
@Transactional(readOnly = true) // read-only hint
public Optional<PersonEntity> findByName(String name) { ... }
@Transactional // explicit write
public PersonEntity save(PersonEntity p) { return repository.save(p); }
}
Neo4jTransactionManager は自動設定されます。明示的な修飾子を指定せずに、JPA の PlatformTransactionManager と混在させないでください。@Transactional はインターフェースではなく、具象クラスに付与してください。
Spring AI — Neo4jVectorStore
依存関係
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-neo4j</artifactId>
</dependency>
application.yml
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
Neo4j 5.15 以降が必要です。spring.neo4j.* の接続設定を再利用します。
使用方法
@Autowired VectorStore vectorStore;
// Store
vectorStore.add(List.of(new Document("text", Map.of("author", "alice"))));
// Similarity search
List<Document> 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()
);
よくあるエラー
| エラー | 原因 | 修正方法 |
|---|---|---|
MappingException: Could not find entity | エンティティがスキャンされていません | @EnableNeo4jRepositories のベースパッケージを確認します |
| 読み込み後にリレーションシップが null になる | デフォルトの深度では深いリレーションシップが省略されることがあります | RETURN m, collect(r), collect(p) を指定した @Query を使用します |
| N+1 クエリ | エンティティごとにリレーションシップを取得しています | 単一の @Query を使うように書き換え、プロジェクションを使用します |
OptimisticLockingFailureException | 同時書き込みにより @Version が古くなっています | サービス層で再試行します |
IllegalStateException: Cannot mix reactive/imperative | 同じコンテキストに両方のリポジトリータイプがあります | どちらか一方のスタックを選択します |
| プロジェクションのフィールドが null になる | ゲッター名が一致していません | ゲッター名をプロパティ名に合わせ、@Property のエイリアスを確認します |
リレーションシップを含む @Query の結果が空になる | collect(r), collect(p) がありません | ルートノード、リレーションシップ、関連ノードをまとめて返します |
Cannot delete node, node has relationships | detach せずに deleteById を使用しています | DETACH DELETE を指定した @Query を使用します |
| トランザクションがロールバックされない | インターフェースに @Transactional が付いています | 具象サービスクラスに適用します |
リレーションシップの読み込み — 重要なルール
SDN は、設定された深度(デフォルトでは 1 ホップ)まで関連エンティティを Eager に読み込みます。より深いグラフの場合は、次のようにします。
// 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<MovieEntity> findByTitleWithCast(String title);
RETURN 内の collect(r), collect(p) は、SDN が @RelationshipProperties を正しくマッピングするために必要です。
参考資料
- Spring Data Neo4j リファレンス(8.x)
- Spring AI Neo4jVectorStore
- GraphAcademy:Spring Data を使った Neo4j アプリの構築
- Neo4j 入門 — SDN
- SDN の高度なプロジェクション
- SDN の監査
- モデリングの落とし穴、プロジェクションガイド、型マッピング
チェックリスト
-
@Nodeにはデフォルトのクラス名ではなく、明示的なラベル文字列を指定する -
@Id @GeneratedValueを使用する(または、楽観的ロックを使うビジネスキーには@Id+@Versionを使用する) -
@RelationshipPropertiesクラスに@RelationshipIdと@TargetNodeがある -
@Relationshipの方向を明示する(OUTGOING / INCOMING) -
@Queryの Cypher では$paramNameを使用し、文字列連結はしない - リレーションシップを多く含む
@Queryでは、ルートノードとともにcollect(r), collect(p)を返す -
application.ymlにデータベース名を設定する(デフォルトの DB が曖昧になるのを防ぎます) - リポジトリー検索で使用するすべてのビジネスキーに対して、DB に一意制約がある
- 具象サービスクラスに
@Transactionalを付ける(インターフェースには付けない) - 同じアプリケーションコンテキストで imperative と reactive を混在させない
- 認証情報は環境変数に置き、
.envを.gitignoreに追加する - 初回実行時は
spring.ai.vectorstore.neo4j.initialize-schema: trueを設定する(Spring AI)