mcpField
N
neo4j-spring-data-skill

neo4j-spring-data-skill

github.com/neo4j-contrib開発118394 回インストール1 週間前に更新
リポジトリ

neo4j-spring-data-skill について

Spring Data Neo4j を使う Spring アプリ開発者向けのスキルです。エンティティマッピング、リポジトリ、トランザクション、リアクティブアクセスに加え、Spring AI の Neo4jVectorStore も扱います。

主な機能

  1. 01Spring Boot と Neo4j の設定
  2. 02ノードとリレーションシップのマッピング
  3. 03リポジトリクエリとプロジェクション
  4. 04リアクティブリポジトリのパターン
  5. 05トランザクション管理の指針
  6. 06Neo4j ベクトル検索の設定

活用シーン

  1. 01Spring アプリの Neo4j 接続設定
  2. 02グラフエンティティと関係のマッピング
  3. 03ページング対応の Cypher リポジトリクエリ作成
  4. 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

バージョンマトリックス

SDNSpring BootSpring FrameworkJavaNeo4j
8.0.x3.3.x / 3.4.x6.2.x17+5.15+
8.1.x3.4.x+7.0.x17+5.15+
7.5.x3.2.x6.1.x17+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 relationshipsdetach せずに 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 を正しくマッピングするために必要です。


参考資料


チェックリスト

  • @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)

コメント