spring-data-neo4j について
Spring Boot アプリケーションに Spring Data Neo4j を統合するためのスキルです。グラフエンティティのマッピング、命令型・リアクティブリポジトリ、Cypher クエリ、組み込みデータベースでのテストを扱います。
主な機能
- 01ノードとリレーションシップのマッピング
- 02命令型・リアクティブリポジトリ
- 03@Query によるカスタム Cypher クエリ
- 04Neo4j Harness を使った統合テスト
活用シーン
- 01Spring Boot アプリへの Neo4j グラフストレージ導入
- 02カスタム Cypher による関連ノードの検索
- 03組み込みデータベースを使ったリポジトリのテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill spring-data-neo4jインストール後、Claude Code で/spring-data-neo4jを入力して読み込まれたか確認します。
同梱ファイルreferences/examples.mdreferences/reference.md
詳細
Spring Data Neo4j 統合パターン
概要
Spring Boot アプリケーション向けの Spring Data Neo4j 統合パターンを提供します。@Node と @Relationship によるノードエンティティのマッピング、リポジトリの設定(命令型およびリアクティブ)、@Query を使ったカスタム Cypher クエリ、組み込み Neo4j データベースによる統合テストを扱います。
使用する場面
次の作業を行う際に、このスキルを使用してください。
- Spring Boot でのグラフデータベースと Neo4j の統合
- ノードエンティティ、リレーションシップ、Cypher クエリ
- Spring Data Neo4j リポジトリ(命令型またはリアクティブ)
- 組み込みデータベースを使用した Neo4j のテスト
手順
1. Spring Data Neo4j をセットアップする
依存関係を追加します。
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'
application.properties で接続を設定します。
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret
Cypher-DSL の方言を設定します(推奨)。
@Configuration
public class Neo4jConfig {
@Bean
Configuration cypherDslConfiguration() {
return Configuration.newConfig()
.withDialect(Dialect.NEO4J_5).build();
}
}
検証チェックポイント:作業を進める前に、cypher-shell で
MATCH (n) RETURN count(n)を実行し、接続が機能することを確認してください。
2. ノードエンティティを定義する
@Node アノテーションを使用してエンティティクラスを指定します- ID の戦略を選択します。
- ビジネスキーを
@Id として使用します(不変の自然識別子) - 生成された
@Id@GeneratedValue を使用します(Neo4j の内部 ID)
- ビジネスキーを
@Relationship アノテーションでリレーションシップを定義します- final フィールドを使ってエンティティを不変にします
- カスタムのプロパティ名には
@Property を使用します
検証チェックポイント:エンティティの保存に失敗する場合は、制約違反がないか確認してください。ID の重複は一意性制約違反になります。
3. リポジトリを作成する
- リポジトリインターフェースを拡張します。
- 命令型の操作には
Neo4jRepository<Entity, ID> - リアクティブな操作には
ReactiveNeo4jRepository<Entity, ID>
- 命令型の操作には
- シンプルなクエリにはクエリ導出を使用します
- 複雑な Cypher クエリには
@Query アノテーションを適用します - パラメーターには
$paramName 構文を使用します
検証チェックポイント:まず
findAll()でリポジトリをテストしてください。結果が空の場合は、Neo4j インスタンスが稼働していることと、認証情報が正しいことを確認してください。
4. 実装をテストする
- テストスライスを使用したリポジトリのテストには
@DataNeo4jTest を使用します - 組み込みデータベースとフィクスチャを使って Neo4j Harness をセットアップします
withFixture()の Cypher クエリでテストデータを用意します- テスト間でテストデータをクリーンアップします
検証チェックポイント:「Connection refused」でテストが失敗する場合は、
@BeforeAllで組み込み Neo4j が正常に起動したことを確認してください。
基本的なエンティティマッピング
ビジネスキーを使ったノードエンティティ
@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;
}
}
生成 ID を使ったノードエンティティ
@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
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);
}
リアクティブリポジトリ
@Repository
public interface MovieRepository extends ReactiveNeo4jRepository<MovieEntity, String> {
Mono<MovieEntity> findOneByTitle(String title);
Flux<MovieEntity> findAllByYear(Integer year);
}
命令型とリアクティブの比較:
- ブロッキングの命令型操作には
Neo4jRepositoryを使用します - ノンブロッキングのリアクティブ操作には
ReactiveNeo4jRepositoryを使用します - 同じアプリケーション内で命令型とリアクティブを混在させないでください
- リアクティブ操作には、データベース側で Neo4j 4 以降が必要です
@Query を使ったカスタムクエリ
@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);
}
カスタムクエリのベストプラクティス:
- パラメータのプレースホルダーには
$parameterNameを使用します - パラメーター名がメソッドのパラメーターと異なる場合は、
@Paramアノテーションを使用します - MATCH はノードのパターンとリレーションシップを指定します
- WHERE は結果を絞り込みます
- RETURN は返す内容を定義します
テスト戦略
統合テストのための Neo4j Harness
テスト設定:
@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");
}
}
例
例 1:エンティティの保存と取得
入力:
MovieEntity movie = new MovieEntity("The Matrix", "Welcome to the Real World", 1999);
movieRepository.save(movie);
MovieEntity found = movieRepository.findOneByTitle("The Matrix");
出力:
MovieEntity{
title="The Matrix",
description="Welcome to the Real World",
year=1999,
actorsAndRoles=[],
directors=[]
}
例 2:カスタム Cypher クエリ
入力:
List<Book> books = authorRepository.findBooksAfterYear("J.R.R. Tolkien", 1950);
出力:
[
Book{isbn="978-0547928210", name="The Fellowship of the Ring", year=1954},
Book{isbn="978-0547928203", name="The Two Towers", year=1956},
Book{isbn="978-0547928227", name="The Return of the King", year=1957}
]
例 3:リレーションシップのたどり方
入力:
@Query("MATCH (m:Movie)<-[:ACTED_IN]-(a:Person) " +
"WHERE m.title = $title RETURN a.name as actorName")
List<String> findActorsByMovieTitle(@Param("title") String title);
List<String> actors = movieRepository.findActorsByMovieTitle("The Matrix");
出力:
["Keanu Reeves", "Laurence Fishburne", "Carrie-Anne Moss", "Hugo Weaving"]
基本から応用まで、映画データベース全体、ソーシャルネットワークのパターン、E コマースの商品カタログ、カスタムクエリ、リアクティブ操作を網羅する例を段階的に紹介します。
包括的なコード例については、examples を参照してください。
ベストプラクティス
エンティティ設計
- 不変エンティティを使用し、フィールドは
finalにします - ビジネスキー(
@Id)または生成 ID(@Id@GeneratedValue)を選択します - エンティティはグラフ構造に集中させ、ビジネスロジックは含めません
- 適切なリレーションシップの方向(INCOMING、OUTGOING、UNDIRECTED)を使用します
リポジトリ設計
- 命令型の場合は
Neo4jRepository、リアクティブの場合はReactiveNeo4jRepositoryを拡張します - 単純なクエリにはクエリ導出を使用します
- 複雑なグラフパターンにはカスタムの
@Query を記述します - 同じアプリケーション内で命令型とリアクティブを混在させないでください
設定
- Cypher-DSL の方言は必ず明示的に設定します
- 認証情報には環境固有のプロパティを使用します
- ソースコードに認証情報をハードコードしないでください
- 負荷に応じてコネクションプールを設定します
テスト
- 統合テストには Neo4j Harness を使用します
withFixture()Cypher クエリでテストデータを用意します- テストのスライスには
@DataNeo4jTestを使用します - 成功するケースとエッジケースの両方をテストします
アーキテクチャ
- コンストラクターインジェクションのみを使用します
- ドメインエンティティと DTO を分離します
- 機能ベースのパッケージ構成に従います
- ドメイン層をフレームワークに依存しない状態に保ちます
セキュリティ
- 認証情報には Spring Boot のプロパティ上書きを使用します
- 適切な認証と認可を設定します
- サービス層で入力パラメーターを検証します
- Cypher インジェクションを防ぐため、パラメーター化されたクエリを使用します
制約と警告
- 同じアプリケーション内で命令型とリアクティブのリポジトリを混在させないでください。
- 書き込み操作には Neo4j のトランザクションが必要です。
@Transactionalが適切に設定されていることを確認してください。 - 深いリレーションシップのトラバーサルはパフォーマンスの問題を引き起こす可能性があるため、注意してください。
- メモリーの問題を避けるため、大量の結果セットはページネーションしてください。
- Cypher クエリでは大文字と小文字が区別されます。プロパティ名の大文字と小文字を一貫させてください。
- 不変エンティティでは、生成 ID に適切な wither メソッドが必要です。
- Spring Data Neo4j のリレーションシップはデフォルトでは遅延ロードされません。大規模なグラフではプロジェクションを検討してください。
- Neo4j Java ドライバーはリアクティブストリームと互換性がありません。リアクティブ操作にはリアクティブドライバーを使用してください。
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
localhost:7687 で Connection refused | Neo4j サーバーが起動していない | Neo4j を起動するか、テストに組み込み Neo4j を使用します |
Authentication failed | 認証情報が間違っている | spring.neo4j.authentication.username/password を確認します |
エンティティが保存されない/MATCH が何も返さない | トランザクションがコミットされていない | @Transactional を追加するか、自動コミットの設定を確認します |
保存時に ConstraintViolationException | @Id の値が重複している | ID が一意であることを確認するか、@GeneratedValue を使用します |
| 結果にリレーションシップが含まれない | @Relationship の方向が間違っている | Direction.INCOMING/OUTGOING/UNDIRECTED を確認します |
@Query が誤ったデータを返す | Cypher のパラメーター構文が間違っている | $paramName を使用し、$ {paramName} は使用しません |
@DataNeo4jTest でテストが失敗する | 組み込み Neo4j が起動していない | テストの前に @BeforeAll で Neo4j が起動することを確認します |
参考資料
API リファレンス全体、Cypher クエリパターン、設定オプションなどの詳細なドキュメントについては、次を参照してください。