© 2008-2024 The original authors.
| このドキュメントのコピーは、あなた自身の使用および他者への配布のために作成することができますが、そのようなコピーに料金を請求しないこと、さらに、印刷物または電子的に配布されるかどうかにかかわらず、各コピーにこの著作権表示が含まれていることを条件とします。 |
序文
1. このドキュメントの手順
このドキュメントは、次のような幅広いユーザー間の橋渡しを試みます。
Spring Framework、Spring Data、コンクリートモジュール (この場合は Spring Data Neo4j)、Neo4j など、すべての Spring エコシステムを初めて使用する人。
Spring Data を初めて使用し、Neo4j の知識を最大限に活用したいと考えている経験豊かな Neo4j 開発者ですが、たとえば宣言型トランザクションを使用する 未知 と、後者を Neo4j クラスター要件に組み込む方法を考えています。
この特定のモジュールと Neo4j を初めて使用し、ビルドブロックがどのように相互作用するかを学ぶ必要がある経験豊富な Spring Data 開発者。このモジュールのプログラミングパラダイムは Spring Data JDBC、Mongo などと非常に一致していますが、クエリ言語 (Cypher)、トランザクションおよびクラスタリングの動作は異なっており、抽象化することはできません。
これらのさまざまなニーズにどのように対応するかは次のとおりです。
Neo4j 固有の質問の多くはよくある質問にあります。これらの質問は、Neo4j 固有の要件をよく知っていて、Spring Data Neo4j でそれらに対処する方法を知りたい人に特に関係があります。
Spring Data の核となる概念をすでに理解している場合は、そのまま第 8 章に進んでください。この章では、アプリケーションを Neo4j インスタンスに接続するように構成するためのさまざまなオプションと、ドメインをモデル化する方法について説明します。
ほとんどの場合、ドメインが必要になります。ノードと関連をドメインモデルにマップする方法については、第 9 章に移動してください。
その後、ドメインをクエリする何らかの手段が必要になります。選択肢は、Neo4j リポジトリ、Neo4j テンプレート、または下位レベルの Neo4j クライアントです。これらはすべて、リアクティブ方式でも利用できます。ページングメカニズムとは別に、標準リポジトリのすべての機能がリアクティブバリアントで利用できます。
古いバージョンの Spring Data Neo4j (通常は SDN+OGM または SDN5 と省略されます) を使用している場合は、SDN の概要、特に SDN+OGM と現在の SDN の間の関連に興味があるでしょう。同じ章では、SDN の構成要素について説明します。
リポジトリの一般的な概念について詳しくは、第 10 章を参照してください。
もちろん、序文と優しい入門ガイドを読み続けてください。
2. Neo4j の導入
グラフデータベースは、膨大な情報ネットワークの保存と取得に特化したストレージエンジンです。データを他のノードまたは同じノードとの関連を持つノードとして効率的に保存するため、それらの構造の高パフォーマンスな検索とクエリが可能になります。プロパティはノードと関連の両方に追加できます。ノードには 0 個以上のラベルを付けることができ、関連は常に指示され、名前が付けられます。
グラフデータベースは、ほとんどの種類のドメインモデルの保存に適しています。ほとんどすべての領域で、特定のものが他のものと接続されています。他のほとんどのモデリング手法では、物事間の関連は、アイデンティティや属性のない単一のリンクに還元されます。グラフデータベースを使用すると、関連を「物」としてモデル化することなく、ドメインに由来する豊富な関連をデータベース内で同様に適切に表現した状態に保つことができます。実際のドメインをグラフデータベースに入れる場合、「インピーダンスの不一致」はほとんどありません。
Neo4j (英語) は、オープンソースの NoSQL グラフデータベースです。これは、関連によって接続されたノードで構成されるグラフとして構造化されたデータを保存する、完全なトランザクションデータベース (ACID) です。現実世界の構造からインスピレーションを得たこのツールは、開発者にとって直感的でシンプルなままでありながら、複雑なデータに対する高いクエリパフォーマンスを可能にします。
Neo4j について学ぶための出発点は neo4j.com (英語) です。役立つリソースのリストは次のとおりです。
Neo4j ドキュメント (英語) には Neo4j が導入されており、スタートガイド、リファレンスドキュメント、チュートリアルへのリンクが含まれています。
オンラインサンドボックス (英語) は、オンラインチュートリアル (英語) と組み合わせて Neo4j インスタンスを操作する便利な方法を提供します。
ネオ 4j Java ボルトドライバー (英語)
数冊の書籍 (英語) を購入したり、ビデオ [YouTube] (英語) を視聴したりできます。
3. Spring Data の導入
Spring Data は、IoC コンテナー、型変換システム、式言語、JMX 統合、ポータブル DAO 例外階層などの Spring Framework のコア機能を使用します。Spring API をすべて理解する必要はありませんが、その背景となる概念を理解することは必要です。少なくとも、IoC の背景となる考え方はよく知っている必要があります。
Spring について詳しくは、Spring Framework について詳しく説明した包括的なドキュメントを参照してください。この問題に関する記事、ブログエントリ、書籍が多数あります。詳細については、Spring Framework ホームページを参照してください。
Spring Data の利点は、同じプログラミングモデルを JPA、JDBC Mongo などのさまざまなストアに適用できることです。そのため、一般的な Spring Data ドキュメントの一部、特に Spring Data リポジトリの操作に関する一般的な章がこのドキュメントに含まれています。これまでに Spring Data モジュールを使用したことがない場合は、必ず参照してください。
4. Spring Data Neo4j の導入
Spring Data Neo4j または略して SDN は、Neo4j 株式会社 (英語) によって作成および保守される次世代の Spring Data モジュールです。VMware Spring Data チームと緊密に連携しています。Neo4j AuraDB を含む、Neo4j の公式にサポートされているすべてのリリースをサポートします。Spring Data Neo4j プロジェクトは、前述の Spring Data の概念を Neo4j グラフデータストアを使用したソリューションの開発に適用します。
SDN は、マッピングフレームワークとドライバーの間に別の「ドライバー」層や「トランスポート」層を導入することなく、Neo4j Java ドライバー [GitHub] (英語) に完全に依存しています。Neo4j Java ドライバー (ボルトまたはボルトドライバーとも呼ばれます) は、JDBC がリレーショナルデータベースで使用されるのとよく似たプロトコルとして使用されます。
SDN は、オブジェクトグラフマッピング (OGM) ライブラリです。OGM は、グラフ内のノードと関連をドメインモデル内のオブジェクトと参照にマップします。オブジェクトインスタンスはノードにマップされ、オブジェクト参照はリレーションシップを使用してマップされるか、プロパティ (日付への参照など) に直列化されます。JVM プリミティブは、ノードまたは関連のプロパティにマップされます。OGM はデータベースを抽象化し、低レベルのドライバーを直接使用することなく、ドメインモデルをグラフに保持し、クエリを実行するための便利なメソッドを提供します。また、SDN によって生成されたクエリでは不十分な場合に、開発者がカスタムクエリを提供できる柔軟性も提供されます。
SDN は以前の SDN バージョン 5 の正式な後継版であり、このドキュメントでは SDN+OGM と呼びます。SDN バージョン 5 では、Spring Data JPA が JPA に関連するのとほぼ同じ方法で、別個のオブジェクトマッピングフレームワークを使用しました。その別名 Neo4j-OGM (Neo4j Object Graph Mapper) は、このモジュール自体に含まれるようになりました。Spring Data Neo4j 自体はオブジェクトマッパーであり、Spring および Spring Boot アプリケーション、および一部のサポートされている Jakarta EE 環境での使用に特化しています。オブジェクトマッパーの個別の実装は必要なく、サポートもされません。
現在の SDN バージョンを以前の SDN+OGM と区別する注目すべき機能は次のとおりです
SDN はそれ自体で完全な OGM です
不変エンティティの完全サポート、Kotlin のデータクラスの完全サポート
Spring Framework 自体および Spring Data でのリアクティブプログラミングモデルの完全なサポート
Neo4j クライアントとリアクティブクライアント機能により、プレーンドライバーにテンプレートの概念が復活し、データベースアクセスが容易になります。
当社は、ドキュメントを保存およびクエリするための高レベルの抽象化としてリポジトリを提供するだけでなく、汎用ドメインアクセスまたは汎用クエリ実行用のテンプレートとクライアントも提供します。これらはすべて、Spring のアプリケーショントランザクションと統合されています。
Neo4j サポートのコア機能は、Neo4jClient または Neo4jTemplate、またはそのリアクティブバリアントを通じて直接使用できます。これらはすべて、Spring のアプリケーションレベルのトランザクションとの統合を提供します。下位レベルでは、Bolt ドライバーインスタンスを取得できますが、この場合は独自のトランザクションを管理する必要があります。
| 最新の Spring Boot アプリケーションでも Neo4j-OGM を使用できます。ただし、SDN 6 以降では使用できません。試行すると、2 つの異なる (無関係な) 永続コンテキストに 2 つの異なるエンティティセットが存在することになります。Neo4j-OGM 3.2.x にこだわりたい場合は、Spring Boot によってインスタンス化された Java ドライバーを使用し、それを Neo4j-OGM セッションに渡します。Neo4j-OGM 3.2.x は引き続きサポートされており、Quarkus などのフレームワークでの使用をお勧めします。ただし、Spring Boot アプリケーションでは、主に SDN を選択する必要があります。 |
必ず参照してください。よくある質問では、マッピングの決定に関する多くの繰り返しの質問に対処するだけでなく、Neo4j AuraDB (英語) やオンプレミスクラスターデプロイなどの Neo4j クラスターインスタンスとの対話を大幅に改善する方法についても説明しています。
理解しておくべき重要な概念は、Neo4j ブックマーク、Spring Retry [GitHub] (英語) や Resilience4j [GitHub] (英語) などの適切な再試行メカニズムを組み込むことの潜在的な必要性 (英語) (この知識は Spring 以外にも適用できるため、後者をお勧めします)、Neo4j クラスターのコンテキストにおける読み取り専用クエリと書き込みクエリの重要性です。
5. Spring Data Neo4j の構成要素
5.1. 概要
SDN は、構成可能なビルドブロックで構成されます。Neo4j Java ドライバー [GitHub] (英語) 上に構築されています。Java ドライバーのインスタンスは、Spring Boot の自動構成自体を通じて提供されます。ドライバーのすべての構成オプションは、ネームスペース spring.neo4j でアクセスできます。ドライバー Bean は、Neo4j と対話するための命令的、非同期、リアクティブなメソッドを提供します。
自動コミットトランザクション (英語) 、トランザクション関数 (英語) 、アンマネージトランザクションなど、ドライバーが Bean で提供するすべてのトランザクションメソッドを使用できます。これらのトランザクションは、進行中の Spring トランザクションと密接ではないことに注意してください。
Spring Data および Spring のプラットフォームまたはリアクティブトランザクションマネージャーとの統合は、Neo4j クライアントから始まります。クライアントは SDN の一部であり、別のスターター spring-boot-starter-data-neo4j を通じて構成されます。そのスターターの構成名前空間は spring.data.neo4j です。
クライアントはマッピングに依存しません。ドメインクラスについては認識されないため、結果をニーズに合ったオブジェクトにマッピングするのはユーザーの責任です。
次に高い抽象レベルは Neo4j テンプレートです。これはドメインを認識しており、それを使用して任意のドメインオブジェクトをクエリできます。このテンプレートは、それぞれ追加のリポジトリ抽象化を作成したくない、多数のドメインクラスまたはカスタムクエリがあるシナリオで役立ちます。
最高レベルの抽象化は Spring Data リポジトリです。
SDN のすべての抽象化は、命令型とリアクティブ型の両方の形式で行われます。同じアプリケーション内で両方のプログラミングスタイルを混在させることはお勧めできません。リアクティブインフラストラクチャには Neo4j 4.0+ データベースが必要です。

5.2. パッケージレベル
| パッケージ | 説明 |
|---|---|
| このパッケージには、アプリケーション固有のアノテーション付き構成クラスに使用できる構成関連のサポートクラスが含まれています。Spring Boot の自動構成に依存しない場合は、抽象基本クラスが役立ちます。このパッケージには、監査を可能にする追加のアノテーションがいくつか用意されています。 |
| このパッケージには、クエリを実行できる命令型またはリアクティブクライアントを作成するためのコアインフラストラクチャが含まれています。 |
| SDN がサポートする単純型のセットを提供します。 |
| このパッケージは、ドメインで役立つ可能性のあるいくつかのサポートクラスを提供します。たとえば、一部のトランザクションが再試行される可能性があることを示す述語や、追加のコンバーターや ID ジェネレーターなどです。 |
| アンマネージド Neo4j トランザクションを Spring マネージドトランザクションに変換するためのコアインフラストラクチャが含まれています。命令型とリアクティブの両方の |
| このパッケージは、Neo4j 命令型およびリアクティブリポジトリ API を提供します。 |
| Neo4j 固有のリポジトリの構成インフラストラクチャ、特に命令型およびリアクティブな Spring Data Neo4j リポジトリを有効にする専用のアノテーション。 |
| このパッケージは、カスタムの命令型およびリアクティブな Spring Data Neo4j リポジトリ基本クラスを構築するためのいくつかのパブリックサポートクラスを提供します。サポートクラスは、SDN 自体で使用されるクラスと同じです。 |
7. 依存関係
個々の Spring Data モジュールの開始日が異なるため、それらのほとんどは異なるメジャーバージョン番号とマイナーバージョン番号を持っています。互換性のあるものを見つける最も簡単な方法は、互換性のあるバージョンが定義された状態で提供される Spring Data リリーストレイン BOM に依存することです。Maven プロジェクトでは、次のように POM の <dependencyManagement /> セクションでこの依存関係を宣言します。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2023.0.11</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement> 現在のリリーストレインバージョンは 2023.0.11 です。トレインバージョンでは、パターン YYYY.MINOR.MICRO の calver (英語) を使用しています。バージョン名は、GA リリースとサービスリリースでは ${calver} に従い、他のすべてのバージョンでは次のパターンに従います。${calver}-${modifier}。modifier は次のいずれかになります。
SNAPSHOT: 現在のスナップショットM1、M2など: マイルストーンRC1、RC2など: リリース候補
Spring Data サンプルリポジトリ [GitHub] (英語) で BOM の使用例を見つけることができます。これが適切な場所にあると、次のように、<dependencies /> ブロックでバージョンなしで使用する Spring Data モジュールを宣言できます。
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>7.1. Spring Boot を使用した依存関係管理
Spring Boot は、Spring Data モジュールの最新バージョンを選択します。それでも新しいバージョンにアップグレードする場合は、spring-data-bom.version プロパティを、使用するトレーニングバージョンとイテレーションに設定します。
詳細については、Spring Boot のドキュメント ( "Spring Data Bom" で検索) を参照してください。
リファレンスドキュメント
誰がこれを読むべきでしょうか ?
このマニュアルは以下を対象として書かれています。
Neo4j の Spring 統合を調査しているエンタープライズアーキテクト。
Neo4j を使用して Spring Data ベースのアプリケーションを開発するエンジニア。
8. 入門
SDN 用に Spring Boot スターターを提供します。依存関係管理を介してスターターモジュールを含め、使用するボルト URL ( spring.neo4j.uri=bolt://localhost:7687 など) を構成してください。スターターは、サーバーが認証を無効にしていると想定します。SDN スターターは Java ドライバーのスターターに依存するため、そこに記載されている設定に関するすべての内容がここにも当てはまります。使用可能なプロパティのリファレンスについては、spring.neo4j 名前空間の IDE の自動補完を使用してください。
SDN のサポート
よく知られ理解されている命令型プログラミングモデル (Spring Data JDBC や JPA によく似ています)
リアクティブトランザクション (英語) の完全サポートを含む、Reactive Streams (英語) に基づくリアクティブプログラミング。
これらはすべて同じバイナリに含まれています。リアクティブプログラミングモデルでは、データベース側に 4 つ以上の Neo4j サーバーが必要で、一方でリアクティブ Spring が必要です。
8.1. データベースを準備する
この例では、すべての Neo4j インスタンスにフリーで付属しているため、ムービーグラフ (英語) 内に留まります。
実行中のデータベースはないが、Docker がインストールされている場合は、次を実行してください。
docker run --publish=7474:7474 --publish=7687:7687 -e 'NEO4J_AUTH=neo4j/secret' neo4j:5 これで http://localhost:7474 にアクセスできるようになります。上記のコマンドは、サーバーのパスワードを secret に設定します。プロンプトでコマンドを実行する準備ができていることに注目してください (:play movies)。これを実行して、データベースにテストデータを入力します。
8.2. 新しい Spring Boot プロジェクトを作成する
Spring Boot プロジェクトをセットアップする最も簡単な方法は start.spring.io です (Web サイトを使用したくない場合に備えて、主要な IDE にも統合されています)。
"Spring Web Starter" を選択して、Spring ベースの Web アプリケーションの作成に必要なすべての依存関係を取得します。Spring Initializr は、選択したビルドツールのすべてのファイルと設定を使用して、有効なプロジェクト構造を作成します。
8.2.1. Maven の使用
Spring イニシャライザーに対して curl リクエストを発行して、基本的な Maven プロジェクトを作成できます。
curl https://start.spring.io/starter.tgz \
-d dependencies=webflux,data-neo4j \
-d bootVersion=3.0.1 \
-d baseDir=Neo4jSpringBootExample \
-d name=Neo4j%20SpringBoot%20Example | tar -xzvf - これにより、新しいフォルダー Neo4jSpringBootExample が作成されます。このスターターはまだイニシャライザーにないため、次の依存関係を pom.xml に手動で追加する必要があります。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>既存のプロジェクトの場合は、依存関係を手動で追加することもできます。
8.2.2. Gradle の使用
考え方は同じで、Gradle プロジェクトを生成するだけです。
curl https://start.spring.io/starter.tgz \
-d dependencies=webflux,data-neo4j \
-d type=gradle-project \
-d bootVersion=3.0.1 \
-d baseDir=Neo4jSpringBootExampleGradle \
-d name=Neo4j%20SpringBoot%20Example | tar -xzvf -Gradle の依存関係は次のようになり、build.gradle に追加する必要があります。
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
}
既存のプロジェクトの場合は、依存関係を手動で追加することもできます。
8.3. プロジェクトを構成する
次に、これらのプロジェクトのいずれかをお気に入りの IDE で開きます。application.properties を見つけて、Neo4j 認証情報を構成します。
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=verysecretこれは、Neo4j インスタンスに接続するために必要な最低限のものです。
| このスターターを使用する場合、ドライバーのプログラム構成を追加する必要はありません。SDN リポジトリは、このスターターによって自動的に有効になります。 |
8.3.1. Neo4j Cypher-DSL の構成
アプリケーションを実行する Neo4j のバージョンに応じて、Neo4j Cypher-DSL を実行するダイアレクトを設定することをお勧めします。デフォルトで使用されるダイアレクトは、Neo4j の LTS バージョンとして Neo4j 4.4 をターゲットにしています。これは、Cypher-DSL Configuration Bean を定義することで変更できます。
@Bean
Configuration cypherDslConfiguration() {
return Configuration.newConfig()
.withDialect(Dialect.NEO4J_5).build();
}
Spring Data Neo4j は、Neo4j 5 とデフォルトのダイアレクトの組み合わせとも互換性があるように最善を尽くしますが、ダイアレクトを明示的に定義することを常にお勧めします。例: これにより、クエリがより最適化され、新しい Neo4j バージョンで elementId() が使用されるようになります。 |
8.4. モジュールパス上で実行
Spring Data Neo4j はモジュールパス上で実行できます。自動モジュール名は spring.data.neo4j です。現在の Spring Data ビルドセットアップの制限により、モジュール自体は提供されません。自動ではあるが安定したモジュール名が使用されます。ただし、モジュール化されたライブラリ ( サイファー DSL [GitHub] (英語) ) に依存します。上記の制限により module-info.java がなければ、お客様に代わってそのライブラリの要件を表明することはできません。
モジュールパスで Spring Data Neo4j 6.1+ を実行するためにプロジェクトで最低限必要な module-info.java は次のとおりです。
module-info.java module your.module {
requires org.neo4j.cypherdsl.core;
requires spring.data.commons;
requires spring.data.neo4j;
opens your.domain to spring.core; (1)
exports your.domain; (2)
}
| 1 | Spring Data Neo4j は Spring Data Commons とそのリフレクティブ機能を使用するため、少なくともドメインパッケージを spring.core に開く必要があります。 |
| 2 | ここでは、your.domain にはリポジトリも含まれていると仮定します。これらは、spring.beans、spring.context、spring.data.commons からアクセスできるようにエクスポートする必要があります。世界にエクスポートしたくない場合は、それらのモジュールに制限できます。 |
8.5. ドメインを作成する
ドメイン層は次の 2 つのことを達成する必要があります。
グラフをオブジェクトにマッピングする
それらへのアクセスを提供する
8.5.1. ノードエンティティの例
SDN は、Kotlin の Java クラスと data クラスの両方で、変更不可能なエンティティを完全にサポートします。ここでは不変エンティティに焦点を当てます。リスト 8 はそのようなエンティティを示します。
| SDN は、Neo4j Java ドライバーがサポートするすべてのデータ型をサポートします。「Cypher 型システム」の章内の Neo4j 型をネイティブ言語型にマッピングする (英語) を参照してください。将来のバージョンでは追加のコンバーターがサポートされる予定です。 |
@Node("Movie") (1)
public class MovieEntity {
@Id (2)
private final String title;
@Property("tagline") (3)
private final String description;
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) (4)
private List<Roles> actorsAndRoles;
@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();
public MovieEntity(String title, String description) { (5)
this.title = title;
this.description = description;
}
// Getters omitted for brevity
}
| 1 | @Node は、このクラスを管理対象エンティティとしてマークするために使用されます。Neo4j ラベルの構成にも使用されます。プレーンな @Node を使用している場合、ラベルはデフォルトでクラスの名前になります。 |
| 2 | 各エンティティには ID が必要です。ここに示すムービークラスは、一意のビジネスキーとして属性 title を使用します。このような一意のキーがない場合は、@Id と @GeneratedValue を組み合わせて、Neo4j の内部 ID を使用するように SDN を構成できます。UUID のジェネレーターも提供します。 |
| 3 | これは、グラフプロパティとは異なるフィールド名を使用する方法として @Property を示しています。 |
| 4 | これは、型 PersonEntity のクラスおよび関連型 ACTED_IN への関連を定義します。 |
| 5 | これは、アプリケーションコードで使用されるコンストラクターです。 |
一般論として: SDN では、データベースによって生成された値をフィールドに設定する方法が必要であるため、内部生成された ID を使用する不変エンティティは少し矛盾しています。
適切なビジネスキーが見つからない場合、または ID のジェネレーターを使用したくない場合は、内部的に生成された ID を通常のコンストラクターと、SDN で使用されるいわゆるウィザー -Method とともに使用する同じエンティティを次に示します。
@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) { (1)
this.id = null;
this.title = title;
this.description = description;
}
public MovieEntity withId(Long id) { (2)
if (this.id.equals(id)) {
return this;
} else {
MovieEntity newObject = new MovieEntity(this.title, this.description);
newObject.id = id;
return newObject;
}
}
}
| 1 | これは、アプリケーションコードで使用されるコンストラクターです。内部 ID を含むフィールドは決して操作されるべきではないため、ID を null に設定します。 |
| 2 | id -attribute のいわゆる枯れです。元のエンティティを変更せずに、新しいエンティティを作成し、それに応じてフィールドを設定することで、エンティティを不変にします。 |
もちろん、Kotlin (英語) で SDN を使用し、Kotlin のデータクラスでドメインをモデル化することもできます。プロジェクト Lombok (英語) は、純粋に Java 内に留まりたい、またはその必要がある場合の代替手段です。
8.5.2. Spring Data リポジトリの宣言
ここには基本的に 2 つのオプションがあります。SDN をストアに依存しないメソッドで作業することと、ドメイン固有の拡張を次の 1 つにすることができます。
org.springframework.data.repository.Repositoryorg.springframework.data.repository.CrudRepositoryorg.springframework.data.repository.reactive.ReactiveCrudRepositoryorg.springframework.data.repository.reactive.ReactiveSortingRepository
それに応じて命令型とリアクティブ型を選択します。
| 技術的には禁止されていませんが、同じアプリケーション内で命令型データベースアクセスとリアクティブデータベースアクセスを混在させることはお勧めできません。このようなシナリオについてはサポートしません。 |
もう 1 つのオプションは、ストア固有の実装に落ち着いて、サポートされているすべてのメソッドをすぐに利用できるようにすることです。このアプローチの利点は最大の欠点でもあります。一度公開されると、これらのメソッドはすべて API の一部になってしまいます。ほとんどの場合、後から何かを追加するよりも、何かを取り除く方が困難です。さらに、ストアの詳細を使用すると、ストアがドメインに漏洩します。パフォーマンスの観点からは、ペナルティはありません。
上記のムービーエンティティのいずれかに適合するリアクティブリポジトリは次のようになります。
public interface MovieRepository extends ReactiveNeo4jRepository<MovieEntity, String> {
Mono<MovieEntity> findOneByTitle(String title);
}
リアクティブコードのテストは reactor.test.StepVerifier を使用して行います。Project Reactor の関連ドキュメント (英語) を参照するか、サンプルコードを参照してください。 |
9. オブジェクトマッピング
次のセクションでは、グラフとドメイン間のマッピングのプロセスについて説明します。このプロセスは 2 つの部分に分かれています。最初の部分では、実際のマッピングと、ノード、関連、プロパティをオブジェクトにマッピングする方法を説明するために使用できるツールについて説明します。2 番目の部分では、Spring Data のオブジェクトマッピングの基礎について説明します。一般的なマッピング、不変のドメインオブジェクトを優先する理由、Java または Kotlin でモデル化する方法など、貴重なヒントが提供されます。
9.1. メタデータベースのマッピング
SDN 内のオブジェクトマッピング機能を最大限に活用するには、マップされたオブジェクトに @Node アノテーションを付ける必要があります。マッピングフレームワークにこのアノテーションがある必要はありませんが (アノテーションがなくても、POJO は正しくマップされます)、クラスパススキャナーがドメインオブジェクトを検索して前処理し、必要なメタデータを抽出できるようになります。このアノテーションを使用しない場合、ドメインオブジェクトを初めて保存するときにアプリケーションのパフォーマンスがわずかに低下します。これは、マッピングフレームワークがドメインオブジェクトのプロパティとその保存メソッドを認識できるように内部メタデータモデルを構築する必要があるためです。持続させます。
9.1.1. マッピングアノテーションの概要
SDN から
@Node: クラスレベルで適用され、このクラスがデータベースへのマッピングの候補であることを示します。@Id: ID 目的に使用されるフィールドをマークするためにフィールドレベルで適用されます。@GeneratedValue:@Idとともにフィールドレベルで適用され、一意の識別子の生成方法を指定します。@Property: 属性からプロパティへのマッピングを変更するためにフィールドレベルで適用されます。@CompositeProperty: コンポジットとして読み取られるマップ型の属性のフィールドレベルで適用されます。複合プロパティを参照してください。@Relationship: フィールドレベルで適用され、関連の詳細を指定します。@DynamicLabels: 動的ラベルのソースを指定するためにフィールドレベルで適用されます。@RelationshipProperties: クラスレベルで適用され、このクラスが関連のプロパティのターゲットとして示されます。@TargetNode:@RelationshipPropertiesアノテーションが付けられたクラスのフィールドに適用され、相手側の観点からその関連のターゲットをマークします。
次のアノテーションは、変換を指定し、OGM との下位互換性を確保するために使用されます。
@DateLong@DateString@ConvertWith
詳細については、"変換" を参照してください。
Spring Data コモンズより
@org.springframework.data.annotation.Idは SDN の@Idと同じですが、実際、@Idには Spring Data Common の ID アノテーションが付けられています。@CreatedBy: ノードの作成者を示すためにフィールドレベルで適用されます。@CreatedDate: ノードの作成日を示すためにフィールドレベルで適用されます。@LastModifiedBy: フィールドレベルで適用され、ノードに対する最後の変更の作成者を示します。@LastModifiedDate: フィールドレベルで適用され、ノードの最終変更日を示します。@PersistenceCreator: 1 つのコンストラクターに適用され、エンティティを読み取るときにそのコンストラクターを優先コンストラクターとしてマークします。@Persistent: クラスレベルで適用され、このクラスがデータベースへのマッピングの候補であることを示します。@Version: フィールドレベルで適用され、オプティミスティックロックに使用され、保存操作時に変更がチェックされます。初期値はゼロで、更新のたびに自動的に値が変更されます。@ReadOnlyProperty: フィールドレベルで適用され、プロパティを読み取り専用としてマークします。プロパティはデータベースの読み取り中にハイドレートされますが、書き込みの対象にはなりません。リレーションシップで使用する場合は、関連性がなければ、そのコレクション内の関連エンティティは永続化されないことに注意してください。
監査サポートに関するすべてのアノテーションについては、第 13 章を参照してください。
9.1.2. 基本的な構成要素: @Node
@Node アノテーションは、マッピングコンテキストによるクラスパススキャンの対象となる、クラスをマネージドドメインクラスとしてマークするために使用されます。
オブジェクトをグラフ内のノードにマッピングしたり、その逆を行うには、マッピング先またはマッピング元のクラスを識別するラベルが必要です。
@Node には、アノテーション付きクラスのインスタンスの読み取りおよび書き込み時に使用される 1 つ以上のラベルを構成できる属性 labels があります。value 属性は labels のエイリアスです。ラベルを指定しない場合は、単純なクラス名がプライマリラベルとして使用されます。複数のラベルを指定したい場合は、次のいずれかを実行できます。
labelsプロパティに配列を指定します。配列内の最初の要素はプライマリラベルとみなされます。primaryLabelに値を指定し、追加のラベルをlabelsに置きます。
プライマリラベルは常に、ドメインクラスを反映する最も具体的なラベルである必要があります。
リポジトリまたは Neo4j テンプレートを通じて書き込まれたアノテーション付きクラスのインスタンスごとに、少なくともプライマリラベルを持つグラフ内の 1 つのノードが書き込まれます。逆に、プライマリラベルを持つすべてのノードは、アノテーションが付けられたクラスのインスタンスにマップされます。
クラス階層に関するメモ
@Node アノテーションはスーパー型およびインターフェースから継承されません。ただし、すべての継承レベルでドメインクラスに個別にアノテーションを付けることができます。これにより、ポリモーフィックなクエリが可能になります。基本クラスまたは中間クラスを渡して、ノードの正しい具体的なインスタンスを取得できます。これは、@Node でアノテーションが付けられた抽象ベースでのみサポートされます。このようなクラスで定義されたラベルは、具体的な実装のラベルとともに追加のラベルとして使用されます。
一部のシナリオでは、ドメインクラス階層のインターフェースもサポートされています。
public interface SomeInterface { (1)
String getName();
SomeInterface getRelated();
}
@Node("SomeInterface") (2)
public static class SomeInterfaceEntity implements SomeInterface {
@Id @GeneratedValue private Long id;
private final String name;
private SomeInterface related;
public SomeInterfaceEntity(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public SomeInterface getRelated() {
return related;
}
}
| 1 | ドメインに名前を付けるような単純なインターフェース名だけです |
| 2 | プライマリラベルを同期する必要があるため、実装クラス (おそらく別のモジュールにある) に @Node を配置します。この値は、実装されたインターフェースの名前とまったく同じであることに注意してください。名前の変更はできません。 |
インターフェース名の代わりに別のプライマリラベルを使用することも可能です。
@Node("PrimaryLabelWN") (1)
public interface SomeInterface2 {
String getName();
SomeInterface2 getRelated();
}
public static class SomeInterfaceEntity2 implements SomeInterface2 {
// Overrides omitted for brevity
}
| 1 | @Node アノテーションをインターフェースに配置します |
インターフェースのさまざまな実装を使用し、ポリモーフドメインモデルを使用することも可能です。その際、少なくとも 2 つのラベルが必要です。1 つはインターフェースを決定するラベル、もう 1 つは具象クラスを決定するものです。
@Node("SomeInterface3") (1)
public interface SomeInterface3 {
String getName();
SomeInterface3 getRelated();
}
@Node("SomeInterface3a") (2)
public static class SomeInterfaceImpl3a implements SomeInterface3 {
// Overrides omitted for brevity
}
@Node("SomeInterface3b") (3)
public static class SomeInterfaceImpl3b implements SomeInterface3 {
// Overrides omitted for brevity
}
@Node
public static class ParentModel { (4)
@Id
@GeneratedValue
private Long id;
private SomeInterface3 related1; (5)
private SomeInterface3 related2;
}
| 1 | このシナリオでは、インターフェースを識別するラベルを明示的に指定する必要があります |
| 2 | 1 番目に当てはまるのは… |
| 3 | そして 2 回目の実装も |
| 4 | これはクライアントまたは親モデルであり、2 つの関連に対して SomeInterface3 を透過的に使用します。 |
| 5 | 具象型は指定されていません |
必要なデータ構造は次のテストに示されています。OGM によっても同じことが書かれます。
Long id;
try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) {
id = transaction.run("" +
"CREATE (s:ParentModel{name:'s'}) " +
"CREATE (s)-[:RELATED_1]-> (:SomeInterface3:SomeInterface3b {name:'3b'}) " +
"CREATE (s)-[:RELATED_2]-> (:SomeInterface3:SomeInterface3a {name:'3a'}) " +
"RETURN id(s)")
.single().get(0).asLong();
transaction.commit();
}
Optional<Inheritance.ParentModel> optionalParentModel = transactionTemplate.execute(tx ->
template.findById(id, Inheritance.ParentModel.class));
assertThat(optionalParentModel).hasValueSatisfying(v -> {
assertThat(v.getName()).isEqualTo("s");
assertThat(v).extracting(Inheritance.ParentModel::getRelated1)
.isInstanceOf(Inheritance.SomeInterfaceImpl3b.class)
.extracting(Inheritance.SomeInterface3::getName)
.isEqualTo("3b");
assertThat(v).extracting(Inheritance.ParentModel::getRelated2)
.isInstanceOf(Inheritance.SomeInterfaceImpl3a.class)
.extracting(Inheritance.SomeInterface3::getName)
.isEqualTo("3a");
});
| インターフェースでは識別子フィールドを定義できません。結果として、それらはリポジトリにとって有効なエンティティ型ではありません。 |
動的または「ランタイム」管理ラベル
単純なクラス名を通じて暗黙的に定義されたラベル、または @Node アノテーションによって明示的に定義されたラベルはすべて静的です。実行中に変更することはできません。実行時に操作できる追加のラベルが必要な場合は、@DynamicLabels を使用できます。@DynamicLabels はフィールドレベルのアノテーションで、型 java.util.Collection<String> (たとえば List または Set) の属性を動的ラベルのソースとしてマークします。
このアノテーションが存在する場合、ノード上に存在し、@Node およびクラス名を介して静的にマップされていないすべてのラベルは、ロード中にそのコレクションに収集されます。書き込み中に、ノードのすべてのラベルは、静的に定義されたラベルとコレクションの内容に置き換えられます。
他のアプリケーションにノードにラベルを追加させる場合は、@DynamicLabels を使用しないでください。@DynamicLabels が管理対象エンティティに存在する場合、結果として得られるラベルのセットはデータベースに書き込まれる「真実」になります。 |
9.1.3. インスタンスの識別: @Id
@Node はクラスと特定のラベルを持つノード間のマッピングを作成しますが、そのクラスの個々のインスタンス (オブジェクト) とノードのインスタンス間の接続も作成する必要があります。
ここで @Id が活躍します。@Id は、クラスの属性をオブジェクトの一意の識別子としてマークします。その一意の識別子は、最適な世界では一意のビジネスキー、言い換えれば自然キーです。@Id は、サポートされている単純型のすべての属性で使用できます。
ただし、自然キーを見つけるのは非常に困難です。たとえば、人の名前は一意であることはほとんどなく、時間の経過とともに変化したり、さらに悪いことに、誰もが姓名を持っているわけではありません。
2 つの異なる種類の代理キーをサポートします。
型 String、long、または Long の属性では、@Id を @GeneratedValue とともに使用できます。Long と long は Neo4j 内部 ID にマップされます。String は Neo4j 5 以降で使用可能な elementId にマップされます。どちらもノードまたはリレーションシップのプロパティではなく、通常は属性には表示されませんが、SDN がクラスの個々のインスタンスを取得できるようにします。
@GeneratedValue は属性 generatorClass を提供します。generatorClass を使用して、IdGenerator を実装するクラスを指定できます。IdGenerator は関数インターフェースであり、その generateId はプライマリラベルとインスタンスを取得して ID を生成します。すぐに使える 1 つの実装として UUIDStringGenerator をサポートします。
generatorRef を介して @GeneratedValue のアプリケーションコンテキストから Spring Bean を指定することもできます。Bean も IdGenerator を実装する必要がありますが、データベースと対話するための Neo4j クライアントやテンプレートなど、コンテキスト内のすべてを利用できます。
| セクション 9.2 での ID の処理に関する重要な注意事項を無視しないでください |
9.1.4. 楽観的ロック: @Version
Spring Data Neo4j は、Long 型付きフィールドで @Version アノテーションを使用することにより、オプティミスティックロックをサポートします。この属性は更新中に自動的に増加するため、手動で変更しないでください。
たとえば、異なるスレッドの 2 つのトランザクションがバージョン x の同じオブジェクトを変更したい場合、最初の操作はデータベースに正常に永続化されます。この時点で、バージョンフィールドはインクリメントされるため、x+1 になります。2 番目の操作は、データベースに存在しないバージョン x のオブジェクトを変更しようとしているため、OptimisticLockingFailureException で失敗します。このような場合、データベースから現在のバージョンのオブジェクトを新たにフェッチすることから始めて、操作を再試行する必要があります。
ビジネス ID を使用する場合は、@Version 属性も必須です。Spring Data Neo4j はこのフィールドをチェックして、エンティティが新しいか、以前にすでに永続化されているかを判断します。
9.1.5. マッピングプロパティ: @Property
@Node アノテーション付きクラスのすべての属性は、Neo4j ノードおよびリレーションシップのプロパティとして保持されます。さらに構成を行わなければ、Java または Kotlin クラスの属性の名前が Neo4j プロパティとして使用されます。
既存の Neo4j スキーマを使用している場合、またはマッピングをニーズに合わせて調整したい場合は、@Property を使用する必要があります。name は、データベース内のプロパティの名前を指定するために使用されます。
9.1.6. ノードの接続: @Relationship
@Relationship アノテーションは、単純型ではないすべての属性で使用できます。これは、@Node のアノテーションが付けられた他の型の属性、またはそのコレクションおよびマップに適用できます。
type または value 属性では関連の型を構成でき、direction では方向を指定できます。SDN のデフォルトの方向は Relationship.Direction#OUTGOING です。
ダイナミックな関連をサポートします。動的関連は Map<String, AnnotatedDomainClass> または Map<Enum, AnnotatedDomainClass> として表されます。このような場合、他のドメインクラスとの関連の型はマップキーによって指定されるため、@Relationship を通じて構成する必要はありません。
マップ関連プロパティ
Neo4j は、ノードだけでなくリレーションシップのプロパティの定義もサポートしています。これらのプロパティをモデルで表現するために、SDN は単純な Java クラスに適用される @RelationshipProperties を提供します。プロパティクラス内には、リレーションシップが指すエンティティを定義するために、@TargetNode としてマークされたフィールドが 1 つだけ存在する必要があります。または、INCOMING 関連のコンテキストでは、から来ています。
リレーションシッププロパティクラスとその使用箇所は次のようになります。
Roles@RelationshipProperties
public class Roles {
@RelationshipId
private Long id;
private final List<String> roles;
@TargetNode
private final PersonEntity person;
public Roles(PersonEntity person, List<String> roles) {
this.person = person;
this.roles = roles;
}
public List<String> getRoles() {
return roles;
}
}
生成された内部 ID (@RelationshipId) のプロパティを定義して、保存中にプロパティを失うことなくどの関連を安全に上書きできるかを SDN が判断できるようにする必要があります。SDN が内部ノード ID を格納するフィールドを見つけられない場合、起動中に失敗します。
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) (1)
private List<Roles> actorsAndRoles;
関連クエリの備考
一般に、クエリを作成するための関連 / ホップに制限はありません。SDN は、モデル化されたノードから到達可能なグラフ全体を解析します。
つまり、関連を双方向にマッピングする、つまりエンティティの両端で関連を定義するという考えがある場合、期待以上の結果が得られる可能性があります。
ムービーに俳優がいて、すべての俳優を含む特定のムービーを取得したい例を考えてみましょう。ムービーから俳優への関連が一方向的なものであれば、これは問題になりません。双方向シナリオでは、SDN は特定の movie、その俳優だけでなく、関連の定義ごとにこの俳優に対して定義された他のムービーも取得します。最悪の場合、これは単一エンティティのグラフ全体のフェッチにまでカスケードされます。
9.1.7. 完全な例
これらすべてを組み合わせると、単純なドメインを作成できます。さまざまなロールを持つムービーや人物を使用します。
MovieEntity@Node("Movie") (1)
public class MovieEntity {
@Id (2)
private final String title;
@Property("tagline") (3)
private final String description;
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) (4)
private List<Roles> actorsAndRoles;
@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();
public MovieEntity(String title, String description) { (5)
this.title = title;
this.description = description;
}
// Getters omitted for brevity
}
| 1 | @Node は、このクラスを管理対象エンティティとしてマークするために使用されます。Neo4j ラベルの構成にも使用されます。プレーンな @Node を使用している場合、ラベルはデフォルトでクラスの名前になります。 |
| 2 | 各エンティティには ID が必要です。ムービーの名前を一意の識別子として使用します。 |
| 3 | これは、グラフプロパティとは異なるフィールド名を使用する方法として @Property を示しています。 |
| 4 | これにより、人との関連が構築されます。 |
| 5 | これは、アプリケーションコードおよび SDN によって使用されるコンストラクターです。 |
ここでは、人々が actors と directors という 2 つのロールにマッピングされています。ドメインクラスは同じです。
PersonEntity@Node("Person")
public class PersonEntity {
@Id private final String name;
private final Integer born;
public PersonEntity(Integer born, String name) {
this.born = born;
this.name = name;
}
public Integer getBorn() {
return born;
}
public String getName() {
return name;
}
}
ムービーと人々の関連を双方向でモデル化したわけではありません。何故ですか? MovieEntity が、関連を所有する集約ルートであると見なされます。一方、すべての人物に関連付けられたムービーを選択せずに、データベースからすべての人物を抽出できるようにしたいと考えています。データベース内のすべての関連をあらゆる方向にマッピングする前に、アプリケーションのユースケースを検討してください。これは可能ですが、オブジェクトグラフ内でグラフデータベースを再構築することになる可能性がありますが、これはマッピングフレームワークの意図したものではありません。循環ドメインまたは双方向ドメインをモデル化する必要があり、グラフ全体をフェッチしたくない場合は、射影を使用してフェッチするデータの詳細な記述を定義できます。 |
9.2. 一意の ID の処理とプロビジョニング
9.2.1. Neo4j 内部 ID の使用
ドメインクラスに一意の識別子を与える最も簡単なメソッドは、型 String または Long のフィールドで @Id と @GeneratedValue を組み合わせることです (インスタンスが新しいかどうかを示すより適切な指標はリテラル null であるため、スカラー long ではなくオブジェクトが望ましい) ):
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private String name;
public MovieEntity(String name) {
this.name = name;
}
}
フィールドに setter を指定する必要はありません。SDN はリフレクションを使用してフィールドを割り当てますが、setter がある場合はそれを使用します。内部生成された ID を使用して不変のエンティティを作成したい場合は、wither を指定する必要があります。
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private final Long id; (1)
private String name;
public MovieEntity(String name) { (2)
this(null, name);
}
private MovieEntity(Long id, String name) { (3)
this.id = id;
this.name = name;
}
public MovieEntity withId(Long id) { (4)
if (this.id.equals(id)) {
return this;
} else {
return new MovieEntity(id, this.title);
}
}
}
| 1 | 生成された値を示す不変の最終 ID フィールド |
| 2 | アプリケーションと Spring Data によって使用されるパブリックコンストラクター |
| 3 | 内部で使用されるコンストラクター |
| 4 | id -attribute のいわゆる枯れです。元のエンティティを変更せずに、新しいエンティティを作成し、それに応じてフィールドを設定することで、エンティティを不変にします。 |
必要な場合は、id 属性に setter を指定するか、wither のようなものを指定する必要があります。
利点: id 属性が代理ビジネスキーであることは明らかであり、それを使用するためにそれ以上の努力や構成は必要ありません。
不利益: これは Neo4js 内部データベース ID に関連付けられていますが、これはデータベースの存続期間全体にわたってのみアプリケーションエンティティに固有のものではありません。
不利益: 不変エンティティを作成するにはより多くの労力が必要です
9.2.2. 外部から提供された代理キーを使用する
@GeneratedValue アノテーションは、org.springframework.data.neo4j.core.schema.IdGenerator を実装するクラスをパラメーターとして受け取ることができます。SDN は、すぐに使用できる InternalIdGenerator (デフォルト) および UUIDStringGenerator を提供します。後者は、エンティティごとに新しい UUID を生成し、java.lang.String として返します。これを使用するアプリケーションエンティティは次のようになります。
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue(UUIDStringGenerator.class)
private String id;
private String name;
}
利点と欠点に関しては、2 つのことを別々に議論する必要があります。割り当て自体と UUID 戦略。普遍的に一意の識別子 [Wikipedia] (英語) は、実用的な目的のために一意であることを目的としています。Wikipedia の言葉を引用すると、「したがって、誰でも UUID を作成し、それを使用して何かを識別することができます。その識別子は、他のものを識別するためにすでに作成された、または今後作成される識別子とほぼ確実に重複しません。」私たちの戦略は Java 内部 UUID メカニズムを使用し、暗号的に強力な擬似乱数ジェネレーターを採用します。ほとんどの場合、これで問題なく動作するはずですが、実際の実行距離は異なる場合があります。
これで割り当て自体は残ります。
アドバンテージ: アプリケーションは完全に制御しており、アプリケーションの目的に十分な一意のキーを生成できます。生成された値は安定しているため、後で変更する必要はありません。
不利益: 生成された戦略はアプリケーション側に適用されます。当時、ほとんどのアプリケーションは適切に拡張するために複数のインスタンスにデプロイされます。戦略が重複を生成する傾向がある場合、主キーの一意性プロパティが違反されるため、挿入は失敗します。このシナリオでは一意のビジネスキーについて考える必要はありませんが、何を生成するかについてはさらに考える必要があります。
独自の ID ジェネレーターを展開するには、いくつかのオプションがあります。1 つはジェネレーターを実装する POJO です。
public class TestSequenceGenerator implements IdGenerator<String> {
private final AtomicInteger sequence = new AtomicInteger(0);
@Override
public String generateId(String primaryLabel, Object entity) {
return StringUtils.uncapitalize(primaryLabel) +
"-" + sequence.incrementAndGet();
}
}
別のオプションは、次のように追加の Spring Bean を提供することです。
@Component
class MyIdGenerator implements IdGenerator<String> {
private final Neo4jClient neo4jClient;
public MyIdGenerator(Neo4jClient neo4jClient) {
this.neo4jClient = neo4jClient;
}
@Override
public String generateId(String primaryLabel, Object entity) {
return neo4jClient.query("YOUR CYPHER QUERY FOR THE NEXT ID") (1)
.fetchAs(String.class).one().get();
}
}
| 1 | 必要なクエリまたはロジックを正確に使用してください。 |
上記のジェネレーターは、次のように Bean リファレンスとして構成されます。
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue(generatorRef = "myIdGenerator")
private String id;
private String name;
}
9.2.3. ビジネスキーの使用
完全な例の MovieEntity および PersonEntity ではビジネスキーを使用しています。個人の名前は、アプリケーションによって、および Spring Data を介してロードされる際の両方で、構築時に割り当てられます。
これは安定した一意のビジネスキーを見つけた場合にのみ可能ですが、優れた不変ドメインオブジェクトが作成されます。
利点: ビジネスキーまたは自然キーを主キーとして使用するのは自然なことです。問題のエンティティは明確に識別されており、ドメインをさらにモデル化する際に、ほとんどの場合、それが適切であると感じられます。
短所: 見つかったキーが思ったほど安定していないことがわかると、主キーとしてのビジネスキーを更新するのは難しくなります。たとえそうでないと約束したとしても、変更される可能性があることが判明することはよくあります。それとは別に、あるものにとって本当に一意な識別子を見つけるのは困難です。
ビジネスキーは、Spring Data Neo4j が処理する前に常にドメインエンティティに設定されることに注意してください。これは、@Version フィールドも提供されない限り、エンティティが新しいかどうかを判断できないことを意味します (エンティティは常に新しいと想定されます)。
9.3. Spring Data オブジェクトマッピングの基礎
このセクションでは、Spring Data オブジェクトのマッピング、オブジェクトの作成、フィールドとプロパティのアクセス、可変性と不変性の基本について説明します。
Spring Data オブジェクトマッピングの中心的なロールは、ドメインオブジェクトのインスタンスを作成し、ストアネイティブデータ構造をそれらにマッピングすることです。つまり、2 つの基本的な手順が必要です。
公開されたコンストラクターの 1 つを使用したインスタンスの作成。
すべての公開されたプロパティを具体化するインスタンスの設定。
9.3.1. オブジェクト作成
Spring Data は、その型のオブジェクトの具体化に使用される永続エンティティのコンストラクターを自動的に検出しようとします。解決アルゴリズムは次のように機能します。
引数のないコンストラクターがある場合は、それが使用されます。他のコンストラクターは無視されます。
引数を取る単一のコンストラクターがある場合は、それが使用されます。
引数を取る複数のコンストラクターがある場合、Spring Data が使用するコンストラクターに
@PersistenceCreatorのアノテーションを付ける必要があります。
値の解決は、コンストラクターの引数名がエンティティのプロパティ名と一致することを前提としています。つまり、マッピングのすべてのカスタマイズ(異なるデータストア列またはフィールド名など)を含む、プロパティが設定されるかのように解決が実行されます。また、これには、クラスファイルで使用可能なパラメーター名情報、またはコンストラクターに存在する @ConstructorProperties アノテーションのいずれかが必要です。
9.3.2. プロパティ設定
エンティティのインスタンスが作成されると、Spring Data はそのクラスの残りのすべての永続プロパティを設定します。エンティティのコンストラクターによってすでに入力されていない場合(つまり、コンストラクターの引数リストを介して使用される場合)、ID プロパティが最初に入力され、循環オブジェクト参照の解決が可能になります。その後、コンストラクターによってまだ設定されていないすべての非一時的なプロパティがエンティティインスタンスに設定されます。そのために、次のアルゴリズムを使用します。
プロパティが不変であってもウィザーメソッド (以下を参照) を公開する場合、ウィザーを使用して新しいプロパティ値を持つ新しいエンティティインスタンスを作成します。
プロパティアクセス (つまり、getter および setter を介したアクセス) が定義されている場合は、setter メソッドを呼び出します。
デフォルトでは、フィールド値を直接設定します。
次のエンティティを見てみましょう。
class Person {
private final @Id Long id; (1)
private final String firstname, lastname; (2)
private final LocalDate birthday;
private final int age; (3)
private String comment; (4)
private @AccessType(Type.PROPERTY) String remarks; (5)
static Person of(String firstname, String lastname, LocalDate birthday) { (6)
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}
Person withId(Long id) { (1)
return new Person(id, this.firstname, this.lastname, this.birthday);
}
void setRemarks(String remarks) { (5)
this.remarks = remarks;
}
}
| 1 | identifier プロパティは final ですが、コンストラクターで null に設定されます。クラスは、識別子の設定に使用される withId(…) メソッドを公開します。インスタンスがデータストアに挿入され、識別子が生成されたとき。元の Vertex インスタンスは、新しいインスタンスが作成されるときに変更されません。通常、ストア管理される他のプロパティにも同じパターンが適用されますが、永続化操作のために変更する必要がある場合があります。 |
| 2 | firstname および lastname プロパティは、getter を介して潜在的に公開される通常の不変のプロパティです。 |
| 3 | age プロパティは不変ですが、birthday プロパティから派生しています。示されている設計では、Spring Data は宣言された唯一のコンストラクターを使用するため、データベース値はデフォルト設定よりも優先されます。計算が優先されることを意図している場合でも、このコンストラクターがパラメーターとして age をとることが重要です(それを無視する可能性がある)。そうしないと、プロパティ生成ステップは年齢フィールドを設定しようとし、不変であり、枯れています。 |
| 4 | comment プロパティは可変であり、フィールドを直接設定することで入力されます。 |
| 5 | remarks プロパティは可変であり、comment フィールドを直接設定するか、setter メソッドを呼び出して設定します。 |
| 6 | このクラスは、オブジェクト作成用のファクトリメソッドとコンストラクターを公開します。ここでの核となる考え方は、追加のコンストラクターの代わりにファクトリメソッドを使用して、@PersistenceCreator によるコンストラクターの明確化の必要性を回避することです。代わりに、プロパティのデフォルト設定はファクトリメソッド内で処理されます。 |
9.3.3. 一般的な推奨事項
不変オブジェクトに固執するようにしてください — 不変オブジェクトは、オブジェクトを実体化するためにコンストラクターを呼び出すだけなので、簡単に作成できます。また、これにより、クライアントコードによるオブジェクトの状態の操作を可能にする setter メソッドがドメインオブジェクトに散在するのを防ぎます。これらが必要な場合は、同じ場所にある限られた種類の型によってのみ呼び出せるように、パッケージで保護することをお勧めします。コンストラクターのみの具体化は、プロパティの作成より最大 30% 高速です。
all-args コンストラクターを提供する — エンティティを不変の値としてモデル化できない、またはしたくない場合でも、オブジェクトのマッピングがプロパティの設定をスキップできるため、エンティティのすべてのプロパティを引数として取るコンストラクターを提供することには価値があります。最適なパフォーマンスのため。
@PersistenceCreatorを回避するために、オーバーロードされたコンストラクターの代わりにファクトリメソッドを使用します — 最適なパフォーマンスに必要なすべての引数コンストラクターでは、通常、自動生成識別子などを省略したアプリケーションユースケース固有のコンストラクターを公開します。これらの all-args コンストラクターのバリアントを公開する静的ファクトリメソッド。生成されたインスタンシエーターおよびプロパティアクセサークラスの使用を許可する制約を必ず遵守してください。
識別子を生成するには、final フィールドを wither メソッドと組み合わせて使用します。
Lombok を使用してボイラープレートコードを回避します — 永続化操作は通常、すべての引数を取るコンストラクターを必要とするため、その宣言はフィールド割り当てに対するボイラープレートパラメーターの退屈な繰り返しとなりますが、Lombok の
@AllArgsConstructorを使用することで回避することができます。
不変マッピングに関する注意事項
可能な限り不変のマッピングと構成を使用することをお勧めしますが、マッピングに関してはいくつかの制限があります。A に B へのコンストラクター参照があり、B に A への参照があるという双方向の関連、またはより複雑なシナリオが考えられます。この鶏が先か卵が先かという状況は、Spring Data Neo4j では解決できません。A のインスタンス化中に、完全にインスタンス化された B が必要になりますが、その一方で、A のインスタンス (正確には同じインスタンス) が必要です。SDN では一般にこのようなモデルが許可されていますが、データベースから返されたデータに上記のようなコンスタレーションが含まれている場合は、実行時に MappingException がスローされます。返されるデータがどのようなものであるかを予測できないようなケースやシナリオでは、リレーションシップの可変フィールドの方が適しています。
9.3.4. Kotlin サポート
Spring Data は、Kotlin の仕様を適合させて、オブジェクトの作成と変更を可能にします。
Kotlin オブジェクトの作成
Kotlin クラスはインスタンス化がサポートされており、すべてのクラスはデフォルトで不変であり、可変プロパティを定義するには明示的なプロパティ宣言が必要です。次の data クラス Vertex を検討してください。
data class Person(val id: String, val name: String)
上記のクラスは、明示的なコンストラクターを持つ典型的なクラスにコンパイルされます。別のコンストラクターを追加してこのクラスをカスタマイズし、@PersistenceCreator でアノテーションを付けてコンストラクターの設定を示します。
data class Person(var id: String, val name: String) {
@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}
Kotlin は、パラメーターが提供されない場合にデフォルト値を使用できるようにすることで、パラメーターのオプションをサポートしています。Spring Data がパラメーターのデフォルト設定を持つコンストラクターを検出した場合、データストアが値を提供しない(または単に null を返す)場合、Kotlin はパラメーターのデフォルト設定を適用できるため、これらのパラメーターは存在しません。name のパラメーターのデフォルト設定を適用する次のクラスを検討してください。
data class Person(var id: String, val name: String = "unknown")
name パラメーターが結果の一部ではないか、その値が null であるたびに、name は unknown にデフォルト設定されます。
10. Spring Data リポジトリの操作
Spring Data リポジトリの抽象化のゴールは、さまざまな永続ストアのデータアクセスレイヤーを実装するために必要な定型コードの量を大幅に削減することです。
Spring Data リポジトリのドキュメントとモジュール この章では、Spring Data リポジトリの中心的な概念とインターフェースについて説明します。この章の情報は、Spring Data Commons モジュールから取得されます。Jakarta Persistence API (JPA) モジュールの構成とコードサンプルを使用します。XML 構成を使用する場合は、XML 名前空間の宣言と拡張する型を、使用する特定のモジュールと同等のものに適合させる必要があります。"[repositories.namespace-reference]" は、リポジトリ API をサポートするすべての Spring Data モジュールでサポートされる XML 構成を対象としています。"付録 B" は、リポジトリの抽象化によって一般的にサポートされているクエリメソッドのキーワードをカバーしています。モジュールの特定の機能の詳細については、このドキュメントのそのモジュールに関する章を参照してください。 |
10.1. コアコンセプト
Spring Data リポジトリ抽象化の中心的なインターフェースは Repository です。管理するドメインクラスと、ドメインクラスの識別子の型を型引数として取ります。このインターフェースは主に、操作する型をキャプチャーし、このインターフェースを継承するインターフェースを検出するのに役立つマーカーインターフェースとして機能します。CrudRepository (Javadoc) および ListCrudRepository (Javadoc) インターフェースは、管理されているエンティティクラスに高度な CRUD 機能を提供します。
CrudRepository インターフェース public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity); (1)
Optional<T> findById(ID primaryKey); (2)
Iterable<T> findAll(); (3)
long count(); (4)
void delete(T entity); (5)
boolean existsById(ID primaryKey); (6)
// … more functionality omitted.
}
| 1 | 指定されたエンティティを保存します。 |
| 2 | 指定された ID で識別されるエンティティを返します。 |
| 3 | すべてのエンティティを返します。 |
| 4 | エンティティの数を返します。 |
| 5 | 指定されたエンティティを削除します。 |
| 6 | 指定された ID のエンティティが存在するかどうかを示します。 |
このインターフェースで宣言されたメソッドは、一般に CRUD メソッドと呼ばれます。ListCrudRepository は同等のメソッドを提供しますが、CrudRepository メソッドが Iterable を返すのに対し、それらは List を返します。
また、JpaRepository や MongoRepository などの永続化技術固有の抽象化も提供します。これらのインターフェースは CrudRepository を継承し、CrudRepository などのかなり汎用的な永続化テクノロジーにとらわれないインターフェースに加えて、基礎となる永続化テクノロジーの機能を公開します。 |
CrudRepository に加えて、エンティティへのページ指定されたアクセスを容易にする追加のメソッドを追加する PagingAndSortingRepository (Javadoc) 抽象化があります。
PagingAndSortingRepository インターフェース public interface PagingAndSortingRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
20 のページサイズで User の 2 番目のページにアクセスするには、次のようなことができます。
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));
クエリメソッドに加えて、カウントクエリと削除クエリの両方のクエリ派生を使用できます。次のリストは、派生カウントクエリのインターフェース定義を示しています。
interface UserRepository extends CrudRepository<User, Long> {
long countByLastname(String lastname);
}
次のリストは、派生削除クエリのインターフェース定義を示しています。
interface UserRepository extends CrudRepository<User, Long> {
long deleteByLastname(String lastname);
List<User> removeByLastname(String lastname);
}
10.2. クエリメソッド
通常、標準の CRUD 機能リポジトリには、基になるデータストアに対するクエリがあります。Spring Data では、これらのクエリを宣言することは 4 ステップのプロセスになります。
次の例に示すように、リポジトリまたはそのサブインターフェースの 1 つを継承するインターフェースを宣言し、処理するドメインクラスと ID 型に入力します。
interface PersonRepository extends Repository<Person, Long> { … }インターフェースでクエリメソッドを宣言します。
interface PersonRepository extends Repository<Person, Long> { List<Person> findByLastname(String lastname); }JavaConfig または XML 構成を使用して、Spring をセットアップして、これらのインターフェースのプロキシインスタンスを作成します。
Listing 17. Java@EnableJpaRepositories class Config { … }Listing 18. XML<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> <repositories base-package="com.acme.repositories"/> </beans>この例では、JPA 名前空間が使用されています。リポジトリの抽象化を他のストアに使用する場合、これをストアモジュールの適切なネームスペース宣言に変更する必要があります。つまり、たとえば
mongodbを優先してjpaを交換する必要があります。アノテーション付きクラスのパッケージがデフォルトで使用されるため、JavaConfig バリアントはパッケージを明示的に構成しないことに注意してください。スキャンするパッケージをカスタマイズするには、データストア固有のリポジトリの
@EnableJpaRepositories-annotation のbasePackage …属性のいずれかを使用します。次の例に示すように、リポジトリインスタンスを挿入して使用します。
class SomeClient { private final PersonRepository repository; SomeClient(PersonRepository repository) { this.repository = repository; } void doSomething() { List<Person> persons = repository.findByLastname("Matthews"); } }
以下のセクションでは、各ステップについて詳しく説明します。
10.3. リポジトリインターフェースの定義
リポジトリインターフェースを定義するには、最初にドメインクラス固有のリポジトリインターフェースを定義する必要があります。インターフェースは Repository を継承し、ドメインクラスと ID 型に入力する必要があります。そのドメイン型の CRUD メソッドを公開する場合は、CrudRepository、または Repository の代わりにそのバリアントの 1 つを継承できます。
10.3.1. リポジトリ定義の微調整
リポジトリインターフェースを使い始める方法にはいくつかのバリエーションがあります。
典型的なアプローチは、CrudRepository を継承することです。これにより、CRUD 機能のメソッドが提供されます。CRUD は、Create、Read、Update、Delete の略です。バージョン 3.0 では、CrudRepository と非常によく似た ListCrudRepository も導入されましたが、複数のエンティティを返すメソッドでは、使いやすい Iterable ではなく List が返されます。
リアクティブストアを使用している場合は、使用しているリアクティブフレームワークに応じて ReactiveCrudRepository または RxJava3CrudRepository を選択できます。
Kotlin を使用している場合は、Kotlin のコルーチンを利用する CoroutineCrudRepository を選択できます。
さらに、Sort 抽象化、または最初の場合は Pageable 抽象化を指定できるメソッドが必要な場合は、PagingAndSortingRepository、ReactiveSortingRepository、RxJava3SortingRepository または CoroutineSortingRepository を継承できます。さまざまな並べ替えリポジトリは、3.0 より前の Spring Data バージョンのようにそれぞれの CRUD リポジトリを継承しなくなったことに注意してください。両方の機能が必要な場合は、両方のインターフェースを継承する必要があります。
Spring Data インターフェースを継承したくない場合は、リポジトリインターフェースに @RepositoryDefinition でアノテーションを付けることもできます。CRUD リポジトリインターフェースの 1 つを継承すると、エンティティを操作するためのメソッドの完全なセットが公開されます。公開するメソッドを選択したい場合は、公開するメソッドを CRUD リポジトリからドメインリポジトリにコピーします。その際、メソッドの戻り値の型を変更できます。Spring Data は、可能であれば戻り値の型を尊重します。例: 複数のエンティティを返すメソッドの場合、Iterable<T>、List<T>、Collection<T> または VAVR リストを選択できます。
アプリケーション内の多くのリポジトリに同じメソッドのセットが必要な場合は、継承元の独自のベースインターフェースを定義できます。このようなインターフェースには、@NoRepositoryBean のアノテーションを付ける必要があります。これにより、Spring Data はインスタンスを直接作成しようとして失敗し、そのリポジトリのエンティティを特定できないために失敗します。これは、ジェネリクス型変数がまだ含まれているためです。
次の例は、CRUD メソッド(この場合は findById および save)を選択的に公開する方法を示しています。
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {
Optional<T> findById(ID id);
<S extends T> S save(S entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}
前の例では、すべてのドメインリポジトリと公開された findById(…) および save(…) に共通のベースインターフェースを定義しました。これらのメソッドは、Spring Data が提供する選択のストアのベースリポジトリ実装にルーティングされます(たとえば、JPA を使用する場合、実装は SimpleJpaRepository です)。これは、それらが CrudRepository のメソッドシグネチャーと一致するためです。そのため、UserRepository はユーザーを保存し、ID で個々のユーザーを検索し、メールアドレスで Users を検索するクエリをトリガーできるようになりました。
中間リポジトリインターフェースには @NoRepositoryBean のアノテーションが付けられています。Spring Data が実行時にインスタンスを作成してはならないすべてのリポジトリインターフェースに、そのアノテーションを必ず追加してください。 |
10.3.2. 複数の Spring Data モジュールでリポジトリを使用する
定義済みスコープ内のすべてのリポジトリインターフェースが Spring Data モジュールにバインドされているため、アプリケーションで一意の Spring Data モジュールを使用すると、物事が簡単になります。アプリケーションによっては、複数の Spring Data モジュールを使用する必要がある場合があります。そのような場合、リポジトリ定義は永続化テクノロジーを区別する必要があります。クラスパスで複数のリポジトリファクトリを検出すると、Spring Data は厳密なリポジトリ構成モードに入ります。厳密な構成では、リポジトリまたはドメインクラスの詳細を使用して、リポジトリ定義の Spring Data モジュールバインディングについて決定します。
リポジトリ定義がモジュール固有のリポジトリを継承する場合、特定の Spring Data モジュールの有効な候補です。
ドメインクラスにモジュール固有の型アノテーションが付けられている場合、そのクラスは特定の Spring Data モジュールの有効な候補となります。Spring Data モジュールは、サードパーティのアノテーション (JPA の
@Entityなど) を受け入れるか、独自のアノテーション (Spring Data MongoDB および Spring Data Elasticsearch の@Documentなど) を提供します。
次の例は、モジュール固有のインターフェース(この場合は JPA)を使用するリポジトリを示しています。
interface MyRepository extends JpaRepository<User, Long> { }
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }
interface UserRepository extends MyBaseRepository<User, Long> { … }
MyRepository および UserRepository は、型階層で JpaRepository を継承します。それらは、Spring Data JPA モジュールの有効な候補です。
次の例は、汎用インターフェースを使用するリポジトリを示しています。
interface AmbiguousRepository extends Repository<User, Long> { … }
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }
interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }
AmbiguousRepository と AmbiguousUserRepository は、型階層内の Repository と CrudRepository のみを継承します。一意の Spring Data モジュールを使用する場合はこれで問題ありませんが、複数のモジュールでは、これらのリポジトリをどの特定の Spring Data にバインドする必要があるかを区別できません。
次の例は、アノテーション付きのドメインクラスを使用するリポジトリを示しています。
interface PersonRepository extends Repository<Person, Long> { … }
@Entity
class Person { … }
interface UserRepository extends Repository<User, Long> { … }
@Document
class User { … }
PersonRepository は、JPA @Entity アノテーションが付けられた Person を参照しているため、このリポジトリは明らかに Spring Data JPA に属しています。UserRepository は、Spring Data MongoDB の @Document アノテーションでアノテーションが付けられた User を参照します。
次の悪い例は、アノテーションが混在するドメインクラスを使用するリポジトリを示しています。
interface JpaPersonRepository extends Repository<Person, Long> { … }
interface MongoDBPersonRepository extends Repository<Person, Long> { … }
@Entity
@Document
class Person { … }
この例は、JPA アノテーションと Spring Data MongoDB アノテーションの両方を使用するドメインクラスを示しています。JpaPersonRepository と MongoDBPersonRepository の 2 つのリポジトリを定義します。1 つは JPA 用で、もう 1 つは MongoDB での使用を目的としています。Spring Data はリポジトリを区別できなくなり、未定義の動作につながります。
リポジトリ型の詳細および識別ドメインクラスアノテーションは、特定の Spring Data モジュールのリポジトリ候補を識別するための厳密なリポジトリ構成に使用されます。同じドメイン型で複数の永続化テクノロジ固有のアノテーションを使用することが可能であり、複数の永続化テクノロジでドメイン型を再利用できます。ただし、Spring Data は、リポジトリをバインドする一意のモジュールを決定できなくなります。
リポジトリを区別する最後の方法は、リポジトリベースパッケージをスコープすることです。ベースパッケージは、リポジトリインターフェース定義のスキャンの開始点を定義します。これは、適切なパッケージにリポジトリ定義があることを意味します。デフォルトのアノテーション駆動型の構成では、構成クラスのパッケージが使用されます。XML ベースの構成の基本パッケージは必須です。
次の例は、基本パッケージのアノテーション駆動型の構成を示しています。
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
10.4. クエリメソッドの定義
リポジトリプロキシには、メソッド名からストア固有のクエリを派生させる 2 つの方法があります。
メソッド名から直接クエリを導出します。
手動で定義されたクエリを使用します。
利用可能なオプションは、実際のストアによって異なります。ただし、作成する実際のクエリを決定する戦略が必要です。次のセクションでは、使用可能なオプションについて説明します。
10.4.1. クエリ検索戦略
クエリを解決するリポジトリインフラストラクチャでは、次の戦略を使用できます。XML 構成では、query-lookup-strategy 属性を使用して名前空間で戦略を構成できます。Java 構成の場合、EnableJpaRepositories アノテーションの queryLookupStrategy 属性を使用できます。特定のデータストアでは一部の戦略がサポートされていない場合があります。
CREATEは、クエリメソッド名からストア固有のクエリを作成しようとします。一般的なアプローチは、メソッド名から既知のプレフィックスの特定のセットを削除し、メソッドの残りを解析することです。クエリ構築の詳細については、"セクション 10.4.2" を参照してください。USE_DECLARED_QUERYは、宣言されたクエリを見つけようとし、見つからない場合は例外をスローします。クエリは、どこかのアノテーションによって定義することも、他の方法で宣言することもできます。そのストアで利用可能なオプションを見つけるには、特定のストアのドキュメントを参照してください。リポジトリインフラストラクチャがブートストラップ時にメソッドに対して宣言されたクエリを見つけられない場合、失敗します。CREATE_IF_NOT_FOUND(デフォルト)は、CREATEとUSE_DECLARED_QUERYを組み合わせたものです。最初に宣言されたクエリを検索し、宣言されたクエリが見つからない場合は、カスタムメソッド名ベースのクエリを作成します。これはデフォルトのルックアップ戦略であるため、明示的に何も構成しない場合に使用されます。メソッド名によるクエリ定義をすばやく行うだけでなく、必要に応じて宣言されたクエリを導入することにより、これらのクエリをカスタム調整することもできます。
10.4.2. クエリ作成
Spring Data リポジトリインフラストラクチャに組み込まれているクエリビルダーメカニズムは、リポジトリのエンティティに対して制約クエリを構築できます。
次の例は、いくつかのクエリを作成する方法を示しています。
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
クエリメソッド名の解析は、主語と述語に分けられます。最初の部分(find … By、exists … By)はクエリのサブジェクトを定義し、2 番目の部分は述語を形成します。導入句(主語)には、さらに式を含めることができます。find (または他の導入キーワード)と By の間のテキストは、Distinct などの結果を制限するキーワードの 1 つを使用して、作成するクエリまたは Top/First は、クエリ結果を制限しますに個別のフラグを設定しない限り、説明的であると見なされます。
付録には、クエリメソッドのサブジェクトキーワードとクエリメソッドの述語キーワードの完全なリストが含まれています。これには、並べ替えや大文字小文字の修飾子が含まれます。ただし、最初の By は、実際の条件述語の開始を示す区切り文字として機能します。非常に基本的なレベルでは、エンティティプロパティの条件を定義し、And および Or と連結できます。
メソッドの解析の実際の結果は、クエリを作成する永続ストアによって異なります。ただし、注意すべき一般的な事項がいくつかあります。
式は通常、連結可能な演算子と組み合わせたプロパティトラバーサルです。プロパティ式を
ANDおよびORと組み合わせることができます。プロパティ式のBetween、LessThan、GreaterThan、Likeなどの演算子もサポートされます。サポートされている演算子はデータストアによって異なる可能性があるため、リファレンスドキュメントの適切な部分を参照してください。メソッドパーサーは、個々のプロパティ(たとえば
findByLastnameIgnoreCase(…))または大文字と小文字の区別をサポートする型のすべてのプロパティ(通常はStringインスタンス -findByLastnameAndFirstnameAllIgnoreCase(…)など)のIgnoreCaseフラグの設定をサポートします。ケースの無視がサポートされているかどうかはストアによって異なるため、ストア固有のクエリメソッドについては、リファレンスドキュメントの関連セクションを参照してください。プロパティを参照するクエリメソッドに
OrderBy句を追加し、並べ替え方向(AscまたはDesc)を提供することにより、静的な順序を適用できます。動的な並べ替えをサポートするクエリメソッドを作成するには、"セクション 10.4.4" を参照してください。
10.4.3. プロパティ式
前の例に示すように、プロパティ式は管理対象エンティティの直接プロパティのみを参照できます。クエリの作成時に、解析されたプロパティが管理対象ドメインクラスのプロパティであることをすでに確認しています。ただし、ネストされたプロパティを走査して制約を定義することもできます。次のメソッドシグネチャーを検討してください。
List<Person> findByAddressZipCode(ZipCode zipCode);
Person に ZipCode を含む Address があると仮定します。その場合、メソッドは x.address.zipCode プロパティトラバーサルを作成します。解決アルゴリズムは、パーツ全体(AddressZipCode)をプロパティとして解釈することから始まり、ドメインクラスでその名前(大文字でない)のプロパティをチェックします。アルゴリズムが成功すると、そのプロパティが使用されます。そうでない場合、アルゴリズムはキャメルケース部分のソースを右側から頭と尾に分割し、対応するプロパティ(この例では、AddressZip と Code)を見つけようとします。アルゴリズムがそのヘッドを持つプロパティを見つけると、テールを取得し、そこからツリーを構築し続け、今説明したメソッドでテールを分割します。最初の分割が一致しない場合、アルゴリズムは分割ポイントを左に移動し(Address、ZipCode)、続行します。
これはほとんどの場合に機能するはずですが、アルゴリズムが間違ったプロパティを選択する可能性があります。Person クラスにも addressZip プロパティがあるとします。アルゴリズムは最初の分割ラウンドですでに一致し、間違ったプロパティを選択して失敗します(addressZip の型にはおそらく code プロパティがないため)。
このあいまいさを解決するには、メソッド名内で _ を使用して、トラバーサルポイントを手動で定義します。メソッド名は次のようになります。
List<Person> findByAddress_ZipCode(ZipCode zipCode);
アンダースコア文字を予約文字として扱うため、標準の Java 命名規則に従うことを強くお勧めします(つまり、プロパティ名にアンダースコアを使用せず、代わりにキャメルケースを使用します)。
10.4.4. ページング、大きな結果の繰り返し、並べ替え
クエリでパラメーターを処理するには、前の例ですでに見たようにメソッドパラメーターを定義します。それに加えて、インフラストラクチャは Pageable や Sort などの特定の型を認識し、ページネーションとソートをクエリに動的に適用します。次の例は、これらの機能を示しています。
Pageable、Slice、Sort の使用 Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
Sort および Pageable を使用する API は、非 null 値がメソッドに渡されることを想定しています。並べ替えやページ付けを適用したくない場合は、Sort.unsorted() と Pageable.unpaged() を使用してください。 |
最初のメソッドでは、org.springframework.data.domain.Pageable インスタンスをクエリメソッドに渡して、静的に定義されたクエリにページングを動的に追加できます。Page は、使用可能な要素とページの総数を認識しています。これは、インフラストラクチャがカウントクエリをトリガーして全体の数を計算することによって行われます。これは(使用するストアによっては)高額になる可能性があるため、代わりに Slice を返すことができます。Slice は、次の Slice が使用可能かどうかのみを認識します。これは、より大きな結果セットをウォークスルーする場合に十分な場合があります。
並べ替えオプションも Pageable インスタンスを介して処理されます。並べ替えのみが必要な場合は、メソッドに org.springframework.data.domain.Sort パラメーターを追加します。ご覧のとおり、List を返すことも可能です。この場合、実際の Page インスタンスを構築するために必要な追加のメタデータは作成されません(つまり、必要だったはずの追加のカウントクエリは発行されません)。むしろ、指定された範囲のエンティティのみを検索するようにクエリを制限します。
| クエリ全体で取得するページ数を調べるには、追加のカウントクエリをトリガーする必要があります。デフォルトでは、このクエリは実際にトリガーするクエリから派生します。 |
どの方法が適切ですか ?
Spring Data 抽象化によって提供される値は、次の表に概説されている可能なクエリメソッドの戻り値の型によっておそらく最もよく示されます。この表は、クエリメソッドから返すことができる型を示しています。
| メソッド | フェッチされたデータの量 | クエリ構造 | 制約 |
|---|---|---|---|
すべての結果。 | 単一のクエリ。 | クエリ結果がすべてのメモリを使い果たす可能性があります。すべてのデータをフェッチすると、時間がかかる場合があります。 | |
すべての結果。 | 単一のクエリ。 | クエリ結果がすべてのメモリを使い果たす可能性があります。すべてのデータをフェッチすると、時間がかかる場合があります。 | |
| 通常はカーソルを使用する単一のクエリ。 | リソースのリークを避けるために、使用後にストリームを閉じる必要があります。 | |
|
| 通常はカーソルを使用する単一のクエリ。 | Store モジュールはリアクティブインフラストラクチャを提供する必要があります。 |
|
| 制限を適用して |
|
|
| 制限を適用する | 多くの場合、コストのかかる
|
ページングとソート
プロパティ名を使用して、簡単な並べ替え式を定義できます。式を連結して、複数の条件を 1 つの式に集めることができます。
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
ソート式を定義するためのより型安全な方法については、ソート式を定義する型から始め、メソッド参照を使用してソートするプロパティを定義します。
TypedSort<Person> person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
TypedSort.by(…) は、(通常)CGlib を使用してランタイムプロキシを利用します。これは、Graal VMNative などのツールを使用するときにネイティブイメージのコンパイルを妨げる可能性があります。 |
ストアの実装が Querydsl をサポートしている場合は、生成されたメタモデル型を使用して、並べ替え式を定義することもできます。
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
10.4.5. クエリ結果の制限
first または top キーワードを使用して、クエリメソッドの結果を制限できます。これらのキーワードは、同じ意味で使用できます。オプションの数値を top または first に追加して、返される最大結果サイズを指定できます。数値が省略されている場合、結果サイズは 1 と見なされます。次の例は、クエリサイズを制限する方法を示しています。
Top および First を使用したクエリの結果サイズの制限 User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
制限式は、個別のクエリをサポートするデータストアの Distinct キーワードもサポートします。また、結果セットを 1 つのインスタンスに制限するクエリの場合、結果を Optional キーワードでラップすることがサポートされています。
ページネーションまたはスライスが制限クエリページネーション(および使用可能なページ数の計算)に適用される場合、制限された結果内で適用されます。
Sort パラメーターを使用して動的ソートと組み合わせて結果を制限すると、"K" 最小エレメントと "K" 最大エレメントの照会メソッドを表現できます。 |
10.4.6. コレクションまたはイテラブルを返すリポジトリメソッド
複数の結果を返すクエリメソッドは、標準の Java Iterable、List、Set を使用できます。さらに、Spring Data の Streamable、Iterable のカスタム拡張、Vavr (英語) によって提供されるコレクション型を返すことをサポートします。考えられるすべてのクエリメソッドの戻り値の型について説明している付録を参照してください。
Streamable をクエリメソッドの戻り値の型として使用する
Iterable または任意のコレクション型の代わりに Streamable を使用できます。これは、非並列 Stream (Iterable にはない)にアクセスするための便利なメソッドと、要素を介して … .filter(…) および … .map(…) を直接アクセスし、Streamable を他の要素に連結する機能を提供します。
interface PersonRepository extends Repository<Person, Long> {
Streamable<Person> findByFirstnameContaining(String firstname);
Streamable<Person> findByLastnameContaining(String lastname);
}
Streamable<Person> result = repository.findByFirstnameContaining("av")
.and(repository.findByLastnameContaining("ea"));
カスタムのストリーミング可能なラッパー型を返す
コレクション専用のラッパー型を提供することは、複数の要素を返すクエリ結果の API を提供するために一般的に使用されるパターンです。通常、これらの型は、コレクションのような型を返すリポジトリメソッドを呼び出し、ラッパー型のインスタンスを手動で作成することによって使用されます。Spring Data では、次の条件を満たす場合、これらのラッパー型をクエリメソッドの戻り値の型として使用できるため、この追加の手順を回避できます。
型は
Streamableを実装します。この型は、
Streamableを引数として取るof(…)またはvalueOf(…)という名前のコンストラクターまたは静的ファクトリメソッドのいずれかを公開します。
次のリストに例を示します。
class Product { (1)
MonetaryAmount getPrice() { … }
}
@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)
private final Streamable<Product> streamable;
public MonetaryAmount getTotal() { (3)
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
@Override
public Iterator<Product> iterator() { (4)
return streamable.iterator();
}
}
interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text); (5)
}
| 1 | 製品の価格にアクセスするための API を公開する Product エンティティ。 |
| 2 | Products.of(…) (Lombok アノテーションを使用して作成されたファクトリメソッド)を使用して構築できる Streamable<Product> のラッパー型。Streamable<Product> を使用する標準のコンストラクターも同様に機能します。 |
| 3 | ラッパー型は追加の API を公開し、Streamable<Product> で新しい値を計算します。 |
| 4 | Streamable インターフェースを実装し、実際の結果に委譲します。 |
| 5 | そのラッパー型 Products は、クエリメソッドの戻り値の型として直接使用できます。Streamable<Product> を返し、リポジトリクライアントでクエリの後に手動でラップする必要はありません。 |
Vavr コレクションのサポート
Vavr (英語) は、Java の関数型プログラミングの概念を取り入れたライブラリです。次の表に示すように、クエリメソッドの戻り値の型として使用できるコレクション型のカスタムセットが付属しています。
| Vavr コレクション型 | 使用される Vavr 実装型 | 有効な Java ソース型 |
|---|---|---|
|
|
|
|
|
|
|
|
|
実際のクエリ結果の Java 型(3 番目の列)に応じて、最初の列の型(またはそのサブ型)をクエリメソッドの戻り値の型として使用し、実装型として使用される 2 番目の列の型を取得できます。または、Traversable (Vavr Iterable と同等)を宣言して、実際の戻り値から実装クラスを導出することもできます。つまり、java.util.List は Vavr List または Seq に変換され、java.util.Set は Vavr LinkedHashSet Set に変換されます。
10.4.7. クエリ結果のストリーミング
戻り値の型として Java 8 Stream<T> を使用することにより、クエリメソッドの結果を段階的に処理できます。次の例に示すように、クエリ結果を Stream でラップする代わりに、データストア固有のメソッドを使用してストリーミングを実行します。
Stream<T> を使用したクエリの結果のストリーミング @Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();
Stream<User> readAllByFirstnameNotNull();
@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
Stream は潜在的に基礎となるデータストア固有のリソースをラップするため、使用後に閉じる必要があります。次の例に示すように、close() メソッドを使用するか、Java 7 try-with-resources ブロックを使用して、Stream を手動で閉じることができます。 |
Stream<T> を操作すると、try-with-resources ブロックが生成されます try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
現在、すべての Spring Data モジュールが戻り型として Stream<T> をサポートしているわけではありません。 |
10.4.8. リポジトリメソッドの null 処理
Spring Data 2.0 以降、個々の集約インスタンスを返すリポジトリ CRUD メソッドは、Java 8 の Optional を使用して、値が存在しない可能性があることを示します。さらに、Spring Data はクエリメソッドで次のラッパー型を返すことをサポートしています。
com.google.common.base.Optionalscala.Optionio.vavr.control.Option
または、クエリメソッドは、ラッパー型をまったく使用しないことを選択できます。null を返すことにより、クエリ結果がないことが示されます。コレクション、コレクションの代替、ラッパー、ストリームを返すリポジトリメソッドは、null ではなく、対応する空の表現を返すことが保証されています。詳細については、"付録 C" を参照してください。
Nullability アノテーション
Spring Framework の nullability アノテーションを使用して、リポジトリメソッドの nullability 制約を表現できます。これらは、次のように、実行時にツールに優しいアプローチとオプトイン null チェックを提供します。
@NonNullApi(Javadoc) : パッケージレベルで使用され、パラメーターと戻り値のデフォルトの動作が、それぞれnull値を受け入れることも生成することもないことを宣言します。@NonNull(Javadoc) :nullであってはならないパラメーターまたは戻り値で使用されます(@NonNullApiが適用されるパラメーターおよび戻り値では不要です)。@Nullable(Javadoc) :nullの可能性があるパラメーターまたは戻り値で使用されます。
Spring アノテーションは、JSR 305 (英語) アノテーション(休止中ですが広く使用されている JSR)でメタアノテーションが付けられています。JSR 305 メタアノテーションにより、ツールベンダー(IDEA (英語) 、Eclipse (英語) 、Kotlin (英語) など)は、Spring アノテーションのサポートをハードコードすることなく、一般的な方法で null-safety サポートを提供できます。クエリメソッドの null 可能性制約のランタイムチェックを有効にするには、次の例に示すように、package-info.java で Spring の @NonNullApi を使用して、パッケージレベルで非 null 可能性をアクティブ化する必要があります。
package-info.java で非 null 可能性を宣言する @org.springframework.lang.NonNullApi
package com.acme;
null 以外のデフォルトが設定されると、リポジトリクエリメソッドの呼び出しは、実行時に null 可能性の制約について検証されます。クエリ結果が定義された制約に違反している場合、例外がスローされます。これは、メソッドが null を返すが、null 許容ではないと宣言されている場合に発生します(リポジトリが存在するパッケージで定義されたアノテーションのデフォルト)。null 許容の結果に再度オプトインする場合は、個々のメソッドで @Nullable を選択的に使用します。このセクションの冒頭で説明した結果ラッパー型を使用すると、引き続き期待どおりに機能します。空の結果は、不在を表す値に変換されます。
次の例は、今説明したいくつかの手法を示しています。
package com.acme; (1)
interface UserRepository extends Repository<User, Long> {
User getByEmailAddress(EmailAddress emailAddress); (2)
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress); (3)
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); (4)
}
| 1 | リポジトリは、null 以外の動作を定義したパッケージ(またはサブパッケージ)にあります。 |
| 2 | クエリで結果が生成されない場合は、EmptyResultDataAccessException をスローします。メソッドに渡された emailAddress が null の場合、IllegalArgumentException をスローします。 |
| 3 | クエリが結果を生成しない場合、null を返します。emailAddress の値として null も受け入れます。 |
| 4 | クエリが結果を生成しない場合、Optional.empty() を返します。メソッドに渡された emailAddress が null の場合、IllegalArgumentException をスローします。 |
Kotlin ベースのリポジトリの Nullability
Kotlin には、言語に組み込まれた null 可能性制約 (英語) の定義があります。Kotlin コードはバイトコードにコンパイルされます。これは、メソッドシグネチャーではなく、コンパイルされたメタデータを通じて nullability 制約を表現しません。kotlin-reflect JAR をプロジェクトに含めて、Kotlin の nullability 制約のイントロスペクションを有効にしてください。Spring Data リポジトリは、言語メカニズムを使用してこれらの制約を定義し、次のように同じランタイムチェックを適用します。
interface UserRepository : Repository<User, String> {
fun findByUsername(username: String): User (1)
fun findByFirstname(firstname: String?): User? (2)
}
| 1 | このメソッドは、パラメーターと結果の両方を null 不可(Kotlin のデフォルト)として定義します。Kotlin コンパイラーは、null をメソッドに渡すメソッド呼び出しを拒否します。クエリで空の結果が得られた場合、EmptyResultDataAccessException がスローされます。 |
| 2 | このメソッドは、firstname パラメーターに null を受け入れ、クエリで結果が生成されない場合は null を返します。 |
10.4.9. 非同期クエリ結果
Spring の非同期メソッド実行機能を使用すると、リポジトリクエリを非同期で実行できます。これは、Spring TaskExecutor に送信されたタスクで実際のクエリが発生している間、メソッドは呼び出し直後に戻ることを意味します。非同期クエリはリアクティブクエリとは異なるため、混在させないでください。リアクティブサポートの詳細については、ストア固有のドキュメントを参照してください。次の例は、いくつかの非同期クエリを示しています。
@Async
Future<User> findByFirstname(String firstname); (1)
@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
| 1 | 戻り値の型として java.util.concurrent.Future を使用します。 |
| 2 | 戻り値の型として Java 8 java.util.concurrent.CompletableFuture を使用します。 |
10.5. リポジトリインスタンスの作成
このセクションでは、定義されたリポジトリインターフェースのインスタンスと Bean 定義を作成する方法について説明します。
10.5.1. Java 構成
Java 構成クラスでストア固有の @EnableJpaRepositories アノテーションを使用して、リポジトリのアクティブ化の構成を定義します。Spring コンテナーの Java ベースの構成の概要については、Spring リファレンスドキュメントの JavaConfig を参照してください。
Spring Data リポジトリを有効にするサンプル構成は次のようになります。
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
上記の例では、JPA 固有のアノテーションを使用しています。これは、実際に使用するストアモジュールに応じて変更します。同じことが EntityManagerFactory Bean の定義にも当てはまります。ストア固有の構成について説明しているセクションを参照してください。 |
10.5.2. XML 構成
次の例に示すように、各 Spring Data モジュールには、Spring がスキャンする基本パッケージを定義できる repositories 要素が含まれています。
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="com.acme.repositories" />
</beans:beans> 前の例では、Spring は、Repository またはそのサブインターフェースの 1 つを継承するインターフェースについて、com.acme.repositories とそのすべてのサブパッケージをスキャンするように指示されています。見つかったインターフェースごとに、インフラストラクチャは永続化テクノロジ固有の FactoryBean を登録して、クエリメソッドの呼び出しを処理する適切なプロキシを作成します。各 Bean は、インターフェース名から派生した Bean 名で登録されるため、UserRepository のインターフェースは userRepository で登録されます。ネストされたリポジトリインターフェースの Bean 名には、囲む型名がプレフィックスとして付けられます。基本パッケージ属性ではワイルドカードを使用できるため、スキャンされたパッケージのパターンを定義できます。
10.5.3. フィルターの使用
デフォルトでは、インフラストラクチャは、構成された基本パッケージにある永続化テクノロジー固有の Repository サブインターフェースを継承するすべてのインターフェースを選択し、そのための Bean インスタンスを作成します。ただし、どのインターフェースに Bean インスタンスが作成されているかをより細かく制御したい場合があります。これを行うには、リポジトリ宣言内でフィルター要素を使用します。セマンティクスは、Spring のコンポーネントフィルターの要素とまったく同じです。詳細については、これらの要素の Spring リファレンスドキュメントを参照してください。
例: 特定のインターフェースをリポジトリ Bean としてインスタンス化から除外するには、次の構成を使用できます。
@Configuration
@EnableJpaRepositories(basePackages = "com.acme.repositories",
includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeRepository") },
excludeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeOtherRepository") })
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
<repositories base-package="com.acme.repositories">
<context:include-filter type="regex" expression=".*SomeRepository" />
<context:exclude-filter type="regex" expression=".*SomeOtherRepository" />
</repositories> 前述の例には、SomeRepository で終わるすべてのインターフェースが含まれており、SomeOtherRepository で終わるインターフェースはインスタンス化から除外されます。
10.5.4. スタンドアロンの使用箇所
Spring コンテナーの外部(CDI 環境など)でリポジトリインフラストラクチャを使用することもできます。クラスパスにはまだいくつかの Spring ライブラリが必要ですが、通常は、プログラムでリポジトリを設定することもできます。リポジトリサポートを提供する Spring Data モジュールには、次のように使用できる永続化テクノロジ固有の RepositoryFactory が付属しています。
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
10.6. Spring Data リポジトリのカスタム実装
Spring Data は、コーディングをほとんど行わずにクエリメソッドを作成するためのさまざまなオプションを提供します。ただし、これらのオプションがニーズに合わない場合は、リポジトリメソッドの独自のカスタム実装を提供することもできます。このセクションでは、その方法について説明します。
10.6.1. 個々のリポジトリのカスタマイズ
カスタム機能でリポジトリを強化するには、最初に、次のように、フラグメントインターフェースとカスタム機能の実装を定義する必要があります。
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
フラグメントインターフェースに対応するクラス名の最も重要な部分は、Impl 後置です。 |
実装自体は Spring Data に依存せず、通常の Spring Bean にすることができます。そのため、標準の依存性注入動作を使用して、他の Bean(JdbcTemplate など)への参照を注入したり、アスペクトに参加したりすることができます。
次に、次のように、リポジトリインターフェースにフラグメントインターフェースを継承させることができます。
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {
// Declare query methods here
}
リポジトリインターフェースでフラグメントインターフェースを拡張すると、CRUD とカスタム機能が組み合わされ、クライアントで使用できるようになります。
Spring Data リポジトリは、リポジトリ構成を形成するフラグメントを使用して実装されます。フラグメントは、基本リポジトリ、機能面(QueryDsl など)、カスタムインターフェースとその実装です。リポジトリインターフェースにインターフェースを追加するたびに、フラグメントを追加して構成を強化します。ベースリポジトリとリポジトリアスペクトの実装は、各 Spring Data モジュールによって提供されます。
次の例は、カスタムインターフェースとその実装を示しています。
interface HumanRepository {
void someHumanMethod(User user);
}
class HumanRepositoryImpl implements HumanRepository {
public void someHumanMethod(User user) {
// Your custom implementation
}
}
interface ContactRepository {
void someContactMethod(User user);
User anotherContactMethod(User user);
}
class ContactRepositoryImpl implements ContactRepository {
public void someContactMethod(User user) {
// Your custom implementation
}
public User anotherContactMethod(User user) {
// Your custom implementation
}
}
次の例は、CrudRepository を継承するカスタムリポジトリのインターフェースを示しています。
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {
// Declare query methods here
}
リポジトリは、宣言の順序でインポートされる複数のカスタム実装で構成されます。カスタム実装は、基本実装およびリポジトリの側面よりも優先度が高くなります。この順序付けにより、ベースリポジトリおよびアスペクトメソッドをオーバーライドし、2 つのフラグメントが同じメソッドシグネチャーを提供する場合のあいまいさを解決できます。リポジトリフラグメントは、単一のリポジトリインターフェースでの使用に限定されません。複数のリポジトリがフラグメントインターフェースを使用し、異なるリポジトリでカスタマイズを再利用できる場合があります。
次の例は、リポジトリフラグメントとその実装を示しています。
save(…) をオーバーライドするフラグメント interface CustomizedSave<T> {
<S extends T> S save(S entity);
}
class CustomizedSaveImpl<T> implements CustomizedSave<T> {
public <S extends T> S save(S entity) {
// Your custom implementation
}
}
次の例は、前述のリポジトリフラグメントを使用するリポジトリを示しています。
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}
interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
構成
リポジトリインフラストラクチャは、リポジトリが見つかったパッケージのクラスをスキャンして、カスタム実装フラグメントを自動検出しようとします。これらのクラスは、デフォルトで Impl になる接尾辞を追加するという命名規則に従う必要があります。
次の例は、デフォルトの接尾辞を使用するリポジトリと、接尾辞のカスタム値を設定するリポジトリを示しています。
@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
<repositories base-package="com.acme.repository" />
<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" /> 前の例の最初の構成は、カスタムリポジトリ実装として機能する com.acme.repository.CustomizedUserRepositoryImpl というクラスを検索しようとします。2 番目の例では、com.acme.repository.CustomizedUserRepositoryMyPostfix を検索しようとします。
あいまいさの解決
一致するクラス名を持つ複数の実装が異なるパッケージで見つかった場合、Spring Data は Bean 名を使用して、使用する実装を識別します。
前に示した CustomizedUserRepository の次の 2 つのカスタム実装を考えると、最初の実装が使用されます。その Bean 名は customizedUserRepositoryImpl であり、これはフラグメントインターフェース(CustomizedUserRepository)の名前と接尾辞 Impl に一致します。
package com.acme.impl.one;
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
package com.acme.impl.two;
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
UserRepository インターフェースに @Component("specialCustom") でアノテーションを付けると、Bean 名に Impl を加えたものが、com.acme.impl.two のリポジトリ実装用に定義されたものと一致し、最初のものの代わりに使用されます。
手動接続
カスタム実装でアノテーションベースの構成とオートワイヤーのみを使用する場合、上記のアプローチは他の Spring Bean と同様に処理されるため、上手く機能します。実装フラグメント Bean に特別な接続が必要な場合、Bean を宣言し、前のセクションで説明した規則に従って名前を付けることができます。インフラストラクチャは、Bean 定義を手動で作成する代わりに、名前で手動で定義したものを参照します。次の例は、カスタム実装を手動で接続する方法を示しています。
class MyClass {
MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
…
}
}
<repositories base-package="com.acme.repository" />
<beans:bean id="userRepositoryImpl" class="…">
<!-- further configuration -->
</beans:bean>10.6.2. ベースリポジトリをカスタマイズする
前のセクションで説明したアプローチでは、ベースリポジトリの動作をカスタマイズしてすべてのリポジトリが影響を受けるようにする場合、各リポジトリインターフェースをカスタマイズする必要があります。代わりに、すべてのリポジトリの動作を変更するために、永続化テクノロジ固有のリポジトリベースクラスを継承する実装を作成できます。このクラスは、次の例に示すように、リポジトリプロキシのカスタムベースクラスとして機能します。
class MyRepositoryImpl<T, ID>
extends SimpleJpaRepository<T, ID> {
private final EntityManager entityManager;
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
このクラスには、ストア固有のリポジトリファクトリ実装が使用するスーパークラスのコンストラクターが必要です。リポジトリの基本クラスに複数のコンストラクターがある場合は、EntityInformation とストア固有のインフラストラクチャオブジェクト(EntityManager またはテンプレートクラスなど)を取得するコンストラクターをオーバーライドします。 |
最後のステップは、カスタマイズされたリポジトリ基本クラスを Spring Data インフラストラクチャに認識させることです。構成では、次の例に示すように、repositoryBaseClass を使用してこれを行うことができます。
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
<repositories base-package="com.acme.repository"
base-class="….MyRepositoryImpl" />10.7. 集約ルートからのイベントの公開
リポジトリによって管理されるエンティティは、集約ルートです。ドメイン駆動設計アプリケーションでは、これらの集約ルートは通常、ドメインイベントを発行します。Spring Data は、@DomainEvents と呼ばれるアノテーションを提供します。これは、次の例に示すように、集約パブリケーションのメソッドで使用して、その公開をできるだけ簡単にすることができます。
class AnAggregateRoot {
@DomainEvents (1)
Collection<Object> domainEvents() {
// … return events you want to get published here
}
@AfterDomainEventPublication (2)
void callbackMethod() {
// … potentially clean up domain events list
}
}
| 1 | @DomainEvents を使用するメソッドは、単一のイベントインスタンスまたはイベントのコレクションのいずれかを返すことができます。引数を取ってはいけません。 |
| 2 | すべてのイベントが公開された後、@AfterDomainEventPublication でアノテーションが付けられたメソッドがあります。これを使用して、公開するイベントのリストを(他の用途の中でも)潜在的にクリーンアップできます。 |
これらのメソッドは、次の Spring Data リポジトリメソッドのいずれかが呼び出されるたびに呼び出されます。
save(…),saveAll(…)delete(…)、deleteAll(…)、deleteAllInBatch(…)、deleteInBatch(…)
これらのメソッドは引数として集約ルートインスタンスを取ることに注意してください。これが、実装がインスタンスを削除するクエリを発行することを選択する可能性があるため、deleteById(…) が特に存在しない理由です。そのため、そもそも集約インスタンスにアクセスできなくなります。
10.8. Spring Data 拡張
このセクションでは、さまざまなコンテキストで Spring Data を使用できるようにする一連の Spring Data 拡張について説明します。現在、ほとんどの統合は Spring MVC を対象としています。
10.8.1. Querydsl 拡張
Querydsl (英語) は、流れるような API を使用して、静的に型指定された SQL のようなクエリの構築を可能にするフレームワークです。
次の例に示すように、いくつかの Spring Data モジュールは、QuerydslPredicateExecutor を介して Querydsl との統合を提供します。
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate); (1)
Iterable<T> findAll(Predicate predicate); (2)
long count(Predicate predicate); (3)
boolean exists(Predicate predicate); (4)
// … more functionality omitted.
}
| 1 | Predicate に一致する単一のエンティティを検索して返します。 |
| 2 | Predicate に一致するすべてのエンティティを検索して返します。 |
| 3 | Predicate に一致するエンティティの数を返します。 |
| 4 | Predicate に一致するエンティティが存在するかどうかを返します。 |
Querydsl サポートを使用するには、次の例に示すように、リポジトリインターフェースで QuerydslPredicateExecutor を継承します。
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
前の例では、次の例に示すように、Querydsl Predicate インスタンスを使用して型安全なクエリを記述できます。
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
10.8.2. Web サポート
リポジトリプログラミングモデルをサポートする Spring Data モジュールには、さまざまな Web サポートが付属しています。Web 関連のコンポーネントでは、Spring MVC JAR がクラスパス上にある必要があります。それらのいくつかは、Spring HATEOAS [GitHub] (英語) との統合さえ提供します。一般に、統合サポートは、次の例に示すように、JavaConfig 構成クラスで @EnableSpringDataWebSupport アノテーションを使用することで有効になります。
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />@EnableSpringDataWebSupport アノテーションは、いくつかのコンポーネントを登録します。これらについては、このセクションの後半で説明します。また、クラスパスで Spring HATEOAS を検出し、統合コンポーネント(存在する場合)も登録します。
基本的な Web サポート
前のセクションで示した構成は、いくつかの基本的なコンポーネントを登録します。
Spring MVC がリクエストパラメーターまたはパス変数からリポジトリ管理ドメインクラスのインスタンスを解決できるようにするセクション 10.8.2.1.1.
Spring MVC がリクエストパラメーターから
PageableおよびSortインスタンスを解決できるようにするHandlerMethodArgumentResolver実装。Jackson モジュールは、使用する Spring Data モジュールに応じて、
PointやDistanceなどの型を逆 / 直列化するか、特定の型を格納します。
DomainClassConverter クラスの使用
DomainClassConverter クラスを使用すると、Spring MVC コントローラーメソッドシグネチャーでドメイン型を直接使用できるため、次の例に示すように、リポジトリからインスタンスを手動で検索する必要がありません。
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
このメソッドは User インスタンスを直接受け取り、それ以上のルックアップは必要ありません。インスタンスは、Spring MVC が最初にパス変数をドメインクラスの id 型に変換し、最終的にドメイン型に登録されたリポジトリインスタンスで findById(…) を呼び出してインスタンスにアクセスすることで解決できます。
現在、リポジトリは変換のために発見される資格があるために CrudRepository を実装しなければなりません。 |
ページング可能およびソート用の HandlerMethodArgumentResolvers
前のセクションで示した構成スニペットは、PageableHandlerMethodArgumentResolver と SortHandlerMethodArgumentResolver のインスタンスも登録します。次の例に示すように、登録により、Pageable および Sort が有効なコントローラーメソッド引数として有効になります。
@Controller
@RequestMapping("/users")
class UserController {
private final UserRepository repository;
UserController(UserRepository repository) {
this.repository = repository;
}
@RequestMapping
String showUsers(Model model, Pageable pageable) {
model.addAttribute("users", repository.findAll(pageable));
return "users";
}
}
上記のメソッドシグネチャーにより、Spring MVC は、次のデフォルト構成を使用して、リクエストパラメーターから Pageable インスタンスを派生させようとします。
| 取得するページ。0 からインデックス付けされ、デフォルトは 0 です。 |
| 取得するページのサイズ。デフォルトは 20 です。 |
|
|
この動作をカスタマイズするには、PageableHandlerMethodArgumentResolverCustomizer インターフェースまたは SortHandlerMethodArgumentResolverCustomizer インターフェースをそれぞれ実装する Bean を登録します。次の例に示すように、customize() メソッドが呼び出され、設定を変更できます。
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}
既存の MethodArgumentResolver のプロパティを設定するだけでは目的に合わない場合は、SpringDataWebConfiguration または HATEOAS 対応の拡張機能を継承し、pageableResolver() または sortResolver() メソッドをオーバーライドし、@Enable アノテーションを使用する代わりにカスタマイズした構成ファイルをインポートします。
リクエストから複数の Pageable または Sort インスタンスを解決する必要がある場合(たとえば、複数のテーブルの場合)、Spring の @Qualifier アノテーションを使用して互いに区別できます。次に、リクエストパラメーターの前に ${qualifier}_ を付ける必要があります。次の例は、結果のメソッドシグネチャーを示しています。
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
thing1_page、thing2_page などを設定する必要があります。
メソッドに渡されるデフォルトの Pageable は PageRequest.of(0, 20) と同等ですが、Pageable パラメーターの @PageableDefault アノテーションを使用してカスタマイズできます。
Page および Slice のハイパーメディアサポート
Spring HATEOAS には、Page または Slice インスタンスのコンテンツを必要な Page/Slice メタデータとリンクで強化して、クライアントがページを簡単にナビゲートできるようにする表現モデルクラス (PagedModel/SlicedModel) が付属しています。Page から PagedModel への変換は、PagedResourcesAssembler と呼ばれる Spring HATEOAS RepresentationModelAssembler インターフェースの実装によって行われます。同様に、Slice インスタンスは SlicedResourcesAssembler を使用して SlicedModel に変換できます。次の例は、SlicedResourcesAssembler がまったく同じように機能するため、PagedResourcesAssembler をコントローラーメソッドの引数として使用する方法を示しています。
@Controller
class PersonController {
private final PersonRepository repository;
// Constructor omitted
@GetMapping("/people")
HttpEntity<PagedModel<Person>> people(Pageable pageable,
PagedResourcesAssembler assembler) {
Page<Person> people = repository.findAll(pageable);
return ResponseEntity.ok(assembler.toModel(people));
}
}
前の例に示すように、構成を有効にすると、PagedResourcesAssembler をコントローラーメソッドの引数として使用できます。その上で toModel(…) を呼び出すと、次の効果があります。
Pageのコンテンツは、PagedModelインスタンスのコンテンツになります。PagedModelオブジェクトはPageMetadataインスタンスをアタッチし、Pageおよび基礎となるPageableからの情報が取り込まれます。PagedModelには、ページの状態に応じて、prevおよびnextリンクが添付される場合があります。リンクは、メソッドがマップする URI を指します。メソッドに追加されたページネーションパラメーターは、PageableHandlerMethodArgumentResolverの設定と一致して、リンクを後で解決できるようにします。
データベースに 30 個の Person インスタンスがあると仮定します。これで、リクエスト(GET http://localhost:8080/people)をトリガーして、次のような出力を確認できます。
{ "links" : [
{ "rel" : "next", "href" : "http://localhost:8080/persons?page=1&size=20" }
],
"content" : [
… // 20 Person instances rendered here
],
"pageMetadata" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
} ここに示されている JSON エンベロープ形式は、正式に指定された構造に従っておらず、安定しているとは保証されておらず、いつでも変更される可能性があります。HAL などの Spring HATEOAS でサポートされている、ハイパーメディア対応の公式メディア型としてレンダリングを有効にすることを強くお勧めします。これらは、@EnableHypermediaSupport アノテーションを使用してアクティブ化できます。詳細については、Spring HATEOAS リファレンスドキュメントを参照してください。 |
アセンブラーは正しい URI を生成し、デフォルト構成を選択して、パラメーターを次のリクエストの Pageable に解決しました。つまり、その構成を変更すると、リンクは自動的に変更に準拠します。デフォルトでは、アセンブラーはそれが呼び出されたコントローラーメソッドを指しますが、ページネーションリンクを構築するためのベースとして使用されるカスタム Link を渡すことにより、それをカスタマイズできます。これにより、PagedResourcesAssembler.toModel(…) メソッドがオーバーロードされます。
Spring Data Jackson モジュール
コアモジュール、および一部のストア固有のモジュールには、Spring Data ドメインで使用される org.springframework.data.geo.Distance や org.springframework.data.geo.Point などの型の Jackson モジュールのセットが付属しています。
これらのモジュールは、Web サポートが有効になり、com.fasterxml.jackson.databind.ObjectMapper が使用可能になるとインポートされます。
初期化中に、SpringDataJacksonConfiguration と同様に SpringDataJacksonModules がインフラストラクチャによって取得されるため、宣言された com.fasterxml.jackson.databind.Module が Jackson ObjectMapper で使用できるようになります。
次のドメイン型のデータバインディングミックスインは、共通のインフラストラクチャによって登録されます。
org.springframework.data.geo.Distance org.springframework.data.geo.Point org.springframework.data.geo.Box org.springframework.data.geo.Circle org.springframework.data.geo.Polygon
個々のモジュールは、追加の |
Web データバインディングのサポート
次の例に示すように、Spring Data 射影(第 11 章で説明)を使用して、JSONPath (英語) 式(Jayway JsonPath [GitHub] (英語) が必要)または XPath [W3C] (英語) 式(XmlBeam (英語) が必要)のいずれかを使用して、受信リクエストペイロードをバインドできます。
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
前の例に示されている型は、Spring MVC ハンドラーメソッドの引数として使用するか、RestTemplate のいずれかのメソッドで ParameterizedTypeReference を使用することで使用できます。上記のメソッド宣言は、指定されたドキュメント内の任意の場所で firstname を見つけようとします。lastname XML ルックアップは、受信ドキュメントのトップレベルで実行されます。の JSON バリアントは、トップレベルの lastname を最初に試行しますが、前者が値を返さない場合は、user サブドキュメントにネストされた lastname も試行します。こうすることで、クライアントが公開メソッドを呼び出さなくても、ソースドキュメントの構造の変更を簡単に軽減できます (通常、クラスベースのペイロードバインディングの欠点です)。
ネストされた射影は、第 11 章に従ってサポートされます。メソッドがインターフェース以外の複雑な型を返す場合、Jackson ObjectMapper が最終値のマッピングに使用されます。
Spring MVC の場合、@EnableSpringDataWebSupport がアクティブになり、必要な依存関係がクラスパスで使用可能になるとすぐに、必要なコンバーターが自動的に登録されます。RestTemplate で使用する場合は、手動で ProjectingJackson2HttpMessageConverter (JSON) または XmlBeamHttpMessageConverter を登録します。
詳細については、標準の Spring Data サンプルリポジトリ [GitHub] (英語) の Web 射影の例 [GitHub] (英語) を参照してください。
Querydsl Web サポート
QueryDSL (英語) が統合されているストアの場合、Request クエリ文字列に含まれている属性からクエリを派生させることができます。
次のクエリ文字列を検討してください。
?firstname=Dave&lastname=Matthews 前の例の User オブジェクトが与えられた場合、次のように QuerydslPredicateArgumentResolver を使用して、クエリ文字列を次の値に解決できます。
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))Querydsl がクラスパスで見つかると、この機能は @EnableSpringDataWebSupport とともに自動的に有効になります。 |
メソッドシグネチャーに @QuerydslPredicate を追加すると、すぐに使用できる Predicate が提供されます。これは、QuerydslPredicateExecutor を使用して実行できます。
型情報は通常、メソッドの戻り値型から解決されます。その情報は必ずしもドメイン型と一致しないため、QuerydslPredicate の root 属性を使用することをお勧めします。 |
次の例は、メソッドシグネチャーで @QuerydslPredicate を使用する方法を示しています。
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, (1)
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
| 1 | User の一致する Predicate にクエリ文字列引数を解決します。 |
デフォルトのバインディングは次のとおりです。
eqとしての単純なプロパティのObject。containsのようなプロパティのようなコレクションのObject。inとしての単純なプロパティのCollection。
これらのバインディングは、@QuerydslPredicate の bindings 属性を使用するか、Java 8 default methods を使用して、次のようにリポジトリインターフェースに QuerydslBinderCustomizer メソッドを追加することによってカスタマイズできます。
interface UserRepository extends CrudRepository<User, String>,
QuerydslPredicateExecutor<User>, (1)
QuerydslBinderCustomizer<QUser> { (2)
@Override
default void customize(QuerydslBindings bindings, QUser user) {
bindings.bind(user.username).first((path, value) -> path.contains(value)) (3)
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
bindings.excluding(user.password); (5)
}
}
| 1 | QuerydslPredicateExecutor は、Predicate の特定のファインダーメソッドへのアクセスを提供します。 |
| 2 | リポジトリインターフェースで定義された QuerydslBinderCustomizer が自動的に選択され、ショートカット @QuerydslPredicate(bindings=…) が選択されます。 |
| 3 | username プロパティのバインディングを単純な contains バインディングとして定義します。 |
| 4 | String プロパティのデフォルトのバインディングを、大文字と小文字を区別しない contains 一致になるように定義します。 |
| 5 | password プロパティを Predicate 解決から除外します。 |
リポジトリまたは @QuerydslPredicate から特定のバインディングを適用する前に、デフォルトの Querydsl バインディングを保持する QuerydslBinderCustomizerDefaults Bean を登録できます。 |
10.8.3. リポジトリポピュレーター
Spring JDBC モジュールを使用している場合は、おそらく DataSource に SQL スクリプトを取り込むためのサポートに精通しているでしょう。同様の抽象化がリポジトリレベルで利用できますが、ストアに依存しない必要があるため、データ定義言語として SQL を使用しません。ポピュレーターは XML(Spring の OXM 抽象化による)と JSON(Jackson による)をサポートして、リポジトリにデータを取り込むデータを定義します。
次の内容の data.json というファイルがあるとします。
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]Spring Data Commons で提供されるリポジトリ名前空間の populator 要素を使用して、リポジトリにデータを取り込むことができます。上記のデータを PersonRepository に入力するには、次のようなポピュレーターを宣言します。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd">
<repository:jackson2-populator locations="classpath:data.json" />
</beans> 上記の宣言により、data.json ファイルは Jackson ObjectMapper によって読み取られ、逆直列化されます。
JSON オブジェクトが非整列化される型は、JSON ドキュメントの _class 属性を調べることで決定されます。インフラストラクチャは最終的に、適切なリポジトリを選択して、デシリアライズされたオブジェクトを処理します。
代わりに、XML を使用してリポジトリにデータを取り込む必要のあるデータを定義するには、unmarshaller-populator エレメントを使用できます。Spring OXM で使用可能な XML マーシャラーオプションの 1 つを使用するように構成します。詳細については、Spring リファレンスドキュメントを参照してください。次の例は、JAXB を使用してリポジトリポピュレータをアンマーシャルする方法を示しています。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xmlns:oxm="http://www.springframework.org/schema/oxm"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd
http://www.springframework.org/schema/oxm
https://www.springframework.org/schema/oxm/spring-oxm.xsd">
<repository:unmarshaller-populator locations="classpath:data.json"
unmarshaller-ref="unmarshaller" />
<oxm:jaxb2-marshaller contextPath="com.acme" />
</beans>10.9. 例示による問い合わせ
10.9.1. 導入
この章では、Query by Example の概要とその使用方法について説明します。
Query by Example(QBE)は、シンプルなインターフェースを備えた使いやすいクエリ手法です。動的なクエリの作成が可能になり、フィールド名を含むクエリを作成する必要がなくなります。実際、Query by Example では、ストア固有のクエリ言語を使用してクエリを記述する必要はまったくありません。
10.9.2. 使用方法
例示による問い合わせ API は、次の 4 つの部分で構成されています。
プローブ: フィールドが設定されたドメインオブジェクトの実際の例。
ExampleMatcher:ExampleMatcherには、特定のフィールドの照合方法に関する詳細が記載されています。複数の例で再利用できます。Example:Exampleは、プローブとExampleMatcherで構成されています。クエリの作成に使用されます。FetchableFluentQuery:FetchableFluentQueryは流れるような API を提供し、Exampleから派生したクエリをさらにカスタマイズできるようにします。Fluent API を使用すると、クエリの順序付けの射影と結果の処理を指定できます。
例示による問い合わせは、いくつかのユースケースに適しています。
静的または動的な制約のセットを使用してデータストアをクエリします。
既存のクエリを壊すことを心配せずにドメインオブジェクトを頻繁にリファクタリングします。
基礎となるデータストア API から独立して動作します。
例示による問い合わせには、いくつかの制限もあります。
firstname = ?0 or (firstname = ?1 and lastname = ?2)など、ネストまたはグループ化されたプロパティ制約はサポートされていません。文字列の starts/contains/ends/regex マッチングと他のプロパティ型の完全一致のみをサポートします。
Query by Example を開始する前に、ドメインオブジェクトが必要です。開始するには、次の例に示すように、リポジトリのインターフェースを作成します。
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}
前の例は、単純なドメインオブジェクトを示しています。これを使用して Example を作成できます。デフォルトでは、null 値を持つフィールドは無視され、文字列はストア固有のデフォルトを使用して照合されます。
例示による問い合わせ条件へのプロパティの包含は、null 可能性に基づいています。プリミティブ型(int、double、…)を使用するプロパティは、ExampleMatcher はプロパティパスを無視しますでない限り、常に含まれます。 |
例は、of ファクトリメソッドを使用するか、ExampleMatcher を使用して作成できます。Example は不変です。次のリストは、簡単な例を示しています。
Person person = new Person(); (1)
person.setFirstname("Dave"); (2)
Example<Person> example = Example.of(person); (3)
| 1 | ドメインオブジェクトの新しいインスタンスを作成します。 |
| 2 | クエリにプロパティを設定します。 |
| 3 | Example を作成します。 |
リポジトリを使用して、サンプルクエリを実行できます。これを行うには、リポジトリインターフェースに QueryByExampleExecutor<T> を継承させます。次のリストは、QueryByExampleExecutor インターフェースからの抜粋を示しています。
QueryByExampleExecutorpublic interface QueryByExampleExecutor<T> {
<S extends T> S findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
// … more functionality omitted.
}
10.9.3. マッチャーの例
例はデフォルト設定に限定されません。次の例に示すように、ExampleMatcher を使用して、文字列照合、null 処理、プロパティ固有の設定に独自のデフォルトを指定できます。
Person person = new Person(); (1)
person.setFirstname("Dave"); (2)
ExampleMatcher matcher = ExampleMatcher.matching() (3)
.withIgnorePaths("lastname") (4)
.withIncludeNullValues() (5)
.withStringMatcher(StringMatcher.ENDING); (6)
Example<Person> example = Example.of(person, matcher); (7)
| 1 | ドメインオブジェクトの新しいインスタンスを作成します。 |
| 2 | セットのプロパティ。 |
| 3 | ExampleMatcher を作成して、すべての値が一致することを期待します。この段階では、さらに構成しなくても使用できます。 |
| 4 | lastname プロパティパスを無視する新しい ExampleMatcher を構築します。 |
| 5 | 新しい ExampleMatcher を作成して、lastname プロパティパスを無視し、null 値を含めます。 |
| 6 | 新しい ExampleMatcher を作成して、lastname プロパティパスを無視し、null 値を含め、サフィックス文字列の照合を実行します。 |
| 7 | ドメインオブジェクトと設定された ExampleMatcher に基づいて新しい Example を作成します。 |
デフォルトでは、ExampleMatcher はプローブに設定されたすべての値が一致することを期待しています。暗黙的に定義された述語のいずれかに一致する結果を取得する場合は、ExampleMatcher.matchingAny() を使用します。
個々のプロパティ(「名」や「姓」、ネストされたプロパティの場合は "address.city" など)の動作を指定できます。次の例に示すように、一致するオプションと大文字と小文字の区別を使用して調整できます。
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}
マッチャーオプションを構成する別の方法は、ラムダ(Java 8 で導入)を使用することです。このアプローチは、実装者にマッチャーの変更を要求するコールバックを作成します。設定オプションはマッチャーインスタンス内に保持されているため、マッチャーを返す必要はありません。次の例は、ラムダを使用するマッチャーを示しています。
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}
Example によって作成されたクエリは、構成の統合ビューを使用します。デフォルトのマッチング設定は ExampleMatcher レベルで設定できますが、個々の設定は特定のプロパティパスに適用できます。ExampleMatcher で設定された設定は、明示的に定義されていない限り、プロパティパス設定に継承されます。プロパティパッチの設定は、デフォルト設定よりも優先されます。次の表は、さまざまな ExampleMatcher 設定の範囲を説明しています。
| 設定 | スコープ |
|---|---|
null ハンドリング |
|
文字列マッチング |
|
Ignoring properties |
Property path |
Case sensitivity |
|
Value transformation |
Property path |
10.9.4. Fluent API
QueryByExampleExecutor offers one more method, which we did not mention so far: <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction).
As with other methods, it executes a query derived from an Example.
However, with the second argument, you can control aspects of that execution that you cannot dynamically control otherwise.
You do so by invoking the various methods of the FetchableFluentQuery in the second argument.
sortBy lets you specify an ordering for your result.
as lets you specify the type to which you want the result to be transformed.
project limits the queried attributes.
first, firstValue, one, oneValue, all, page, stream, count, and exists define what kind of result you get and how the query behaves when more than the expected number of results are available.
Optional<Person> match = repository.findBy(example,
q -> q
.sortBy(Sort.by("lastname").descending())
.first()
);
10.10. Spring Data Neo4j Extensions
10.10.1. Available extensions for Spring Data Neo4j repositories
Spring Data Neo4j offers a couple of extensions or "mixins" that can be added to repositories. What is a mixin? According to Wikipedia (英語) mixins are a language concept that allows a programmer to inject some code into a class. Mixin programming is a style of software development, in which units of functionality are created in a class and then mixed in with other classes.
Java does not support that concept on the language level, but we do emulate it via a couple of interfaces and a runtime that adds appropriate implementations and interceptors for.
Mixins added by default are QueryByExampleExecutor and ReactiveQueryByExampleExecutor respectively. Those interfaces are
explained in detail in Section 10.9.
Additional mixins provided are:
-
QuerydslPredicateExecutor CypherdslConditionExecutorCypherdslStatementExecutorReactiveQuerydslPredicateExecutorReactiveCypherdslConditionExecutorReactiveCypherdslStatementExecutor
生成されたクエリに動的条件を追加する
QuerydslPredicateExecutor と CypherdslConditionExecutor はどちらも同じ概念を提供します。つまり、SDN がクエリを生成し、追加される「述語」 (Query DSL) または「条件」 (Cypher DSL) を指定します。Cypher DSL は SDN がネイティブに使用するものであるため、Cypher DSL をお勧めします。静的メタモデルを生成するアノテーションプロセッサー (英語) の使用を検討することもできます。
それはどのように機能するのでしょうか ? 上で説明したようにリポジトリを宣言し、次のインターフェースのいずれかを追加します。
interface QueryDSLPersonRepository extends
Neo4jRepository<Person, Long>, (1)
QuerydslPredicateExecutor<Person> { (2)
}
| 1 | 標準リポジトリ宣言 |
| 2 | クエリ DSL ミックスイン |
OR
interface PersonRepository extends
Neo4jRepository<Person, Long>, (1)
CypherdslConditionExecutor<Person> { (2)
}
| 1 | 標準リポジトリ宣言 |
| 2 | Cypher DSL ミックスイン |
Cypher DSL 条件エグゼキュータを使用した使用例を示します。
Node person = Cypher.node("Person").named("person"); (1)
Property firstName = person.property("firstName"); (2)
Property lastName = person.property("lastName");
assertThat(
repository.findAll(
firstName.eq(Cypher.anonParameter("Helge"))
.or(lastName.eq(Cypher.parameter("someName", "B."))), (3)
lastName.descending() (4)
))
.extracting(Person::getFirstName)
.containsExactly("Helge", "Bela");
| 1 | クエリのルートをターゲットとする名前付き Node オブジェクトを定義します |
| 2 | そこからいくつかのプロパティを導き出す |
| 3 | or 条件を作成します。名には匿名パラメーターが使用され、姓には名前付きパラメーターが使用されます。これは、これらのフラグメントでパラメーターを定義する方法であり、それができない Query-DSL ミックスインに比べて利点の 1 つです。リテラルは Cypher.literalOf で表現できます。 |
| 4 | いずれかのプロパティから SortItem を定義します |
このコードは、Query-DSL ミックスインの場合とよく似ています。Query-DSL ミックスインの理由としては、API が使い慣れていることと、他のストアでも動作することが考えられます。これに反対する理由は、クラスパス上に追加のライブラリが必要であること、リレーションシップのトラバースがサポートされていないこと、および述語内のパラメーターをサポートしていないこと (技術的にはサポートされていますが、API メソッドが存在しないこと) です。実際に実行中のクエリに渡します)。
エンティティと射影に対する (動的) Cypher-DSL ステートメントの使用
対応するミックスインを追加することは、条件エクゼキュータを使用することと変わりません。
interface PersonRepository extends
Neo4jRepository<Person, Long>,
CypherdslStatementExecutor<Person> {
}
ReactiveNeo4jRepository を延長する場合は ReactiveCypherdslStatementExecutor をご使用ください。
CypherdslStatementExecutor には、findOne および findAll 用のいくつかのオーバーロードが付属しています。これらはすべて、Cypher-DSL ステートメントをそれぞれ最初のパラメーターとして、その進行中の定義を受け取り、投影メソッドの場合は型を受け取ります。
クエリにパラメーターが必要な場合は、次のように、パラメーターは Cypher-DSL 自体を介して定義され、Cypher-DSL によって設定される必要があります。
static Statement whoHasFirstNameWithAddress(String name) { (1)
Node p = Cypher.node("Person").named("p"); (2)
Node a = Cypher.anyNode("a");
Relationship r = p.relationshipTo(a, "LIVES_AT");
return Cypher.match(r)
.where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))) (3)
.returning(
p.getRequiredSymbolicName(),
Functions.collect(r),
Functions.collect(a)
)
.build();
}
@Test
void fineOneShouldWork(@Autowired PersonRepository repository) {
Optional<Person> result = repository.findOne(whoHasFirstNameWithAddress("Helge")); (4)
assertThat(result).hasValueSatisfying(namesOnly -> {
assertThat(namesOnly.getFirstName()).isEqualTo("Helge");
assertThat(namesOnly.getLastName()).isEqualTo("Schneider");
assertThat(namesOnly.getAddress()).extracting(Person.Address::getCity)
.isEqualTo("Mülheim an der Ruhr");
});
}
@Test
void fineOneProjectedShouldWork(@Autowired PersonRepository repository) {
Optional<NamesOnly> result = repository.findOne(
whoHasFirstNameWithAddress("Helge"),
NamesOnly.class (5)
);
assertThat(result).hasValueSatisfying(namesOnly -> {
assertThat(namesOnly.getFirstName()).isEqualTo("Helge");
assertThat(namesOnly.getLastName()).isEqualTo("Schneider");
assertThat(namesOnly.getFullName()).isEqualTo("Helge Schneider");
});
}
| 1 | 動的クエリはヘルパーメソッドで型 セーフな方法で構築されます |
| 2 | これについてはすでに here で確認しましたが、そこではモデルを保持するいくつかの変数も定義しました |
| 3 | メソッドに渡される name の実際の値が入る匿名パラメーターを定義します |
| 4 | ヘルパーメソッドから返されたステートメントは、エンティティを検索するために使用されます。 |
| 5 | あるいは射影。 |
findAll メソッドも同様に機能します。命令型 Cypher-DSL ステートメントエグゼキューターは、ページングされた結果を返すオーバーロードも提供します。
11. 射影
Spring Data クエリメソッドは通常、リポジトリによって管理される集約ルートの 1 つまたは複数のインスタンスを返します。ただし、これらの型の特定の属性に基づいて射影を作成することが望ましい場合があります。Spring Data では、専用の戻り値型をモデル化して、管理対象集合体の部分ビューをより選択的に取得できます。
次の例のようなリポジトリおよび集約ルート型を想像してください。
class Person {
@Id UUID id;
String firstname, lastname;
Address address;
static class Address {
String zipCode, city, street;
}
}
interface PersonRepository extends Repository<Person, UUID> {
Collection<Person> findByLastname(String lastname);
}
ここで、人の名前属性のみを取得することを想像してください。Spring Data はこれを達成するためにどのような意味を持っていますか? この章の残りはその質問に回答します。
11.1. インターフェースベースの射影
クエリの結果を名前属性のみに制限する最も簡単な方法は、次の例に示すように、読み取るプロパティのアクセサーメソッドを公開するインターフェースを宣言することです。
interface NamesOnly {
String getFirstname();
String getLastname();
}
ここで重要なことは、ここで定義されたプロパティが集約ルートのプロパティと正確に一致することです。これにより、クエリメソッドを次のように追加できます。
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
クエリ実行エンジンは、返された各要素に対して実行時にそのインターフェースのプロキシインスタンスを作成し、公開されたメソッドへの呼び出しをターゲットオブジェクトに転送します。
基本メソッド(たとえば、CrudRepository、ストア固有のリポジトリインターフェース、Simple … Repository で宣言されている)をオーバーライドするメソッドを Repository で宣言すると、宣言された戻り値の型に関係なく、基本メソッドが呼び出されます。基本メソッドは射影に使用できないため、互換性のある戻り値の型を使用してください。一部のストアモジュールは、@Query アノテーションをサポートして、オーバーライドされたベースメソッドをクエリメソッドに変換します。このクエリメソッドを使用して、射影を返すことができます。 |
射影は再帰的に使用できます。Address 情報の一部も含めたい場合は、次の例に示すように、そのための射影インターフェースを作成し、getAddress() の宣言からそのインターフェースを返します。
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
メソッドの呼び出し時に、ターゲットインスタンスの address プロパティが取得され、順番に投影プロキシにラップされます。
11.1.1. 閉じた射影
アクセサーメソッドがすべてターゲット集合体のプロパティに一致する射影インターフェースは、閉じた射影と見なされます。次の例(この章の前半でも使用しました)は、閉じた射影です。
interface NamesOnly {
String getFirstname();
String getLastname();
}
閉じた射影を使用する場合、Spring Data はクエリの実行を最適化できます。これは、射影プロキシのバックアップに必要なすべての属性がわかっているためです。詳細については、リファレンスドキュメントのモジュール固有の部分を参照してください。
11.1.2. 開いた射影
次の例に示すように、@Value アノテーションを使用して、射影インターフェースのアクセサーメソッドを使用して新しい値を計算することもできます。
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
射影を支える集約ルートは、target 変数で利用可能です。@Value を使用した射影インターフェースは、オープン射影です。この場合、Spring Data はクエリ実行最適化を適用できません。これは、SpEL 式が集約ルートの任意の属性を使用できるためです。
@Value で使用される式は複雑すぎてはいけません — String 変数でのプログラミングは避けたいです。非常に単純な式の場合、次の例に示すように、1 つのオプションはデフォルトのメソッド(Java 8 で導入)に頼ることです。
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}
このアプローチでは、射影インターフェースで公開される他のアクセサーメソッドに純粋に基づいてロジックを実装できる必要があります。次の例に示すように、2 番目のより柔軟なオプションは、Spring Bean にカスタムロジックを実装し、SpEL 式からそれを呼び出すことです。
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
SpEL 式が myBean を参照し、getFullName(…) メソッドを呼び出し、射影ターゲットをメソッドパラメーターとして転送する方法に注目してください。SpEL 式の評価に裏付けられたメソッドは、メソッドパラメーターを使用することもできます。このパラメーターは、式から参照できます。メソッドのパラメーターは、args という名前の Object 配列を介して使用できます。次の例は、args 配列からメソッドパラメーターを取得する方法を示しています。
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
繰り返しますが、より複雑な式の場合は、前に説明したように、Spring Bean を使用し、式でメソッドを呼び出す必要があります。
11.1.3. null 可能ラッパー
射影インターフェースの Getter は、null 許容ラッパーを使用して null の安全性を向上させることができます。現在サポートされているラッパー型は次のとおりです。
java.util.Optionalcom.google.common.base.Optionalscala.Optionio.vavr.control.Option
interface NamesOnly {
Optional<String> getFirstname();
}
基になる射影値が null でない場合、値はラッパー型の現在の表現を使用して返されます。バッキング値が null の場合、getter メソッドは使用されたラッパー型の空の表現を返します。
11.2. クラスベースの射影 (DTO)
射影を定義するもう 1 つの方法は、取得することになっているフィールドのプロパティを保持する値型 DTO(データ転送オブジェクト)を使用することです。これらの DTO 型は、プロキシが発生せず、ネストされた射影を適用できないことを除いて、射影インターフェースとまったく同じ方法で使用できます。
ストアがロードするフィールドを制限することでクエリの実行を最適化する場合、ロードされるフィールドは公開されているコンストラクターのパラメーター名から決定されます。
次の例は、投影 DTO を示しています。
record NamesOnly(String firstname, String lastname) {
}
Java レコードは、値のセマンティクスに準拠しているため、DTO 型を定義するのに理想的です。すべてのフィールドは private final であり、equals(…)/hashCode()/toString() メソッドは自動的に作成されます。または、投影するプロパティを定義する任意のクラスを使用できます。
11.3. 動的射影
これまで、コレクションの戻り値型または要素型として射影型を使用しました。ただし、呼び出し時に使用する型を選択することもできます(これにより、動的になります)。動的射影を適用するには、次の例に示すようなクエリメソッドを使用します。
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
この方法では、次の例に示すように、メソッドを使用して、そのままで、または射影を適用して集約を取得できます。
void someMethod(PersonRepository people) {
Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}
型 Class のクエリパラメーターは、動的射影パラメーターとして適格かどうかがインスペクションされます。クエリの実際の戻り値の型が Class パラメーターのジェネリクスパラメーター型と等しい場合、一致する Class パラメーターはクエリまたは SpEL 式内で使用できません。Class パラメーターをクエリ引数として使用する場合は、必ず別のジェネリクスパラメーター(Class<?> など)を使用してください。 |
11.4. 総論
上で記述されていたように、射影には、インターフェースと DTO ベースの射影という 2 つの種類があります。Spring Data Neo4j では、両方の型の射影が、どのプロパティと関連がワイヤ上で転送されるかに直接影響します。アプリケーションのすべての使用シナリオで必要ではない可能性のある多くのプロパティを含むノードとエンティティを扱う場合、どちらのアプローチでもデータベースの負荷を軽減できます。
インターフェースと DTO ベースの射影の両方で、Spring Data Neo4j はクエリの構築にリポジトリのドメイン型を使用します。クエリを変更する可能性のあるすべての属性のすべてのアノテーションが考慮されます。ドメイン型は、リポジトリ宣言によって定義された型です ( interface TestRepository extends CrudRepository<TestEntity, Long> のような宣言がある場合、ドメイン型は TestEntity になります)。
インターフェースベースの射影は常に、基礎となるドメイン型に対する動的プロキシになります。このようなインターフェース ( getName など) で定義されたアクセサーの名前は、射影されたエンティティに存在するプロパティ (ここでは name) に解決される必要があります。共通の Spring Data インフラストラクチャを通じてアクセスできる限り、これらのプロパティにドメイン型にアクセサーがあるかどうかは関係ありません。後者は、そもそもドメイン型が永続的なエンティティではないため、すでに保証されています。
DTO ベースの射影は、カスタムクエリで使用すると多少柔軟性が高くなります。標準クエリは元のドメイン型から派生しているため、そこで定義されているプロパティと関連のみを使用できますが、カスタムクエリは追加のプロパティを追加できます。
ルールは次のとおりです。まず、ドメイン型のプロパティを使用して DTO を設定します。DTO がアクセサーまたはフィールドを介して追加のプロパティを宣言した場合、Spring Data Neo4j は結果のレコード内で一致するプロパティを探します。プロパティは名前が正確に一致する必要があり、単純な型 ( org.springframework.data.neo4j.core.convert.Neo4jSimpleTypes で定義されている) または既知の永続エンティティのいずれかにすることができます。これらのコレクションはサポートされていますが、マップはサポートされていません。
11.5. マルチレベル射影
Spring Data Neo4j はマルチレベル射影もサポートしています。
interface ProjectionWithNestedProjection {
String getName();
List<Subprojection1> getLevel1();
interface Subprojection1 {
String getName();
List<Subprojection2> getLevel2();
}
interface Subprojection2 {
String getName();
}
}
循環射影をモデル化したり、循環を作成するエンティティを指定したりすることは可能ですが、射影ロジックはそれらの循環には従わず、循環のないクエリを作成するだけです。
マルチレベル射影は、射影すべきエンティティに限定されます。この場合、RelationshipProperties はエンティティのカテゴリに分類され、射影が適用される場合は考慮される必要があります。
11.6. 射影のデータ操作
射影を DTO としてフェッチした場合は、その値を変更できます。ただし、インターフェースベースの射影を使用している場合は、インターフェースを更新するだけでは済みません。使用できる一般的なパターンは、インターフェースを使用し、インターフェースからコピーされた値を使用してドメインエンティティを作成するメソッドをドメインエンティティクラスに提供することです。このようにして、次のセクションで説明するように、エンティティを更新し、射影ブループリント / マスクを使用してエンティティを再度永続化できます。
11.7. 射影の永続性
射影によるデータの取得と同様に、永続化の青写真としても使用できます。Neo4jTemplate は、これらの射影を保存操作に適用するための流れるような API を提供します。
特定のドメインクラスの射影を保存することもできます
Projection projection = neo4jTemplate.save(DomainClass.class).one(projectionValue);
または、ドメインオブジェクトを保存しても、射影で定義されたフィールドのみを考慮することもできます。
Projection projection = neo4jTemplate.saveAs(domainObject, Projection.class);
どちらの場合も、コレクションベースの操作にも使用でき、射影で定義されたフィールドとリレーションシップのみが更新されます。
| データの削除 (関連の削除など) を防ぐために、後で永続化する必要がある少なくともすべてのデータを常にロードする必要があります。 |
11.8. 完全な例
次のエンティティ、射影、対応するリポジトリがあるとします。
@Node
class TestEntity {
@Id @GeneratedValue private Long id;
private String name;
@Property("a_property") (1)
private String aProperty;
}
| 1 | このプロパティはグラフ内では異なる名前になっています |
TestEntity から継承した派生エンティティ @Node
class ExtendedTestEntity extends TestEntity {
private String otherAttribute;
}
TestEntity のインターフェース射影 interface TestEntityInterfaceProjection {
String getName();
}
TestEntity の DTO 射影(追加属性を 1 つ含む)class TestEntityDTOProjection {
private String name;
private Long numberOfRelations; (1)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getNumberOfRelations() {
return numberOfRelations;
}
public void setNumberOfRelations(Long numberOfRelations) {
this.numberOfRelations = numberOfRelations;
}
}
| 1 | この属性は投影されたエンティティに存在しません |
TestEntity のリポジトリは以下に示されており、リストで説明したように動作します。
TestEntity のリポジトリ interface TestRepository extends CrudRepository<TestEntity, Long> { (1)
List<TestEntity> findAll(); (2)
List<ExtendedTestEntity> findAllExtendedEntities(); (3)
List<TestEntityInterfaceProjection> findAllInterfaceProjectionsBy(); (4)
List<TestEntityDTOProjection> findAllDTOProjectionsBy(); (5)
@Query("MATCH (t:TestEntity) - [r:RELATED_TO] -> () RETURN t, COUNT(r) AS numberOfRelations") (6)
List<TestEntityDTOProjection> findAllDTOProjectionsWithCustomQuery();
}
| 1 | リポジトリのドメイン型は TestEntity です |
| 2 | 1 つ以上の TestEntity を返すメソッドは、ドメイン型に一致するため、そのインスタンスのみを返します。 |
| 3 | ドメイン型を継承するクラスの 1 つ以上のインスタンスを返すメソッドは、拡張したクラスのインスタンスのみを返します。問題のメソッドのドメイン型は、リポジトリ自体のドメイン型を満たす拡張クラスになります。 |
| 4 | このメソッドはインターフェース射影を返すため、メソッドの戻り値の型はリポジトリのドメインの型とは異なります。インターフェースは、ドメイン型で定義されたプロパティにのみアクセスできます。サフィックス By は、SDN が TestEntity で InterfaceProjections というプロパティを検索しないようにするために必要です。 |
| 5 | このメソッドは DTO 射影を返します。これを実行すると、DTO がドメイン型の契約にない追加属性として numberOfRelations を定義しているため、SDN が警告を発行します。TestEntity 内のアノテーション付き属性 aProperty は、クエリ内で a_property に正しく変換されます。上記のように、戻り値の型はリポジトリのドメインの型とは異なります。サフィックス By は、SDN が TestEntity 内の DTOProjections というプロパティを検索しないようにするために必要です。 |
| 6 | このメソッドは DTO 射影も返します。ただし、クエリには射影で定義された追加属性に適合する値が含まれているため、警告は発行されません。 |
| 上記のリストのリポジトリでは、具体的な戻り値の型を使用して射影を定義していますが、別のバリエーションとして、Spring Data Neo4j が他の Spring Data プロジェクトと共有するドキュメントの一部で説明されている動的射影の使用があります。動的射影は、クローズドインターフェース射影とオープンインターフェース射影の両方に適用できるほか、クラスベースの DTO 射影にも適用できます。 動的射影の鍵は、 <T> Collection<T> findByName(String name, Class<T> type) のように、リポジトリ内のクエリメソッドへの最後のパラメーターとして目的の射影型を指定することです。これは、上記の TestRepository に追加できる宣言で、複数のメソッドで可能な @Query アノテーションを繰り返すことなく、同じメソッドで取得されるさまざまな射影を可能にします。 |
12. テスト
12.1. Spring Boot なし
独自の統合テストの構成のために抽象基本クラスを頻繁に使用します。それらは次のように使用できます。
@ExtendWith(SpringExtension.class)
class YourIntegrationTest {
@Test
void thingsShouldWork(@Autowired Neo4jTemplate neo4jTemplate) {
// Add your test
}
@Configuration
@EnableNeo4jRepositories(considerNestedRepositories = true)
@EnableTransactionManagement
static class Config extends AbstractNeo4jConfig {
@Bean
public Driver driver() {
return GraphDatabase.driver("bolt://yourtestserver:7687", AuthTokens.none()); (1)
}
}
}
ここで、テストサーバーまたはコンテナーへの接続を提供する必要があります。
同様のクラスがリアクティブテスト用に提供されています。
12.2. Spring Boot、@DataNeo4jTest の場合
Spring Boot は @DataNeo4jTest から org.springframework.boot:spring-boot-starter-test まで提供します。後者は、アノテーションと必要なインフラストラクチャコードを含む org.springframework.boot:spring-boot-test-autoconfigure を組み込みます。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
@DataNeo4jTest は Spring Boot テストスライスです。テストスライスは、Neo4j を使用したテストに必要なすべてのインフラストラクチャ、つまりトランザクションマネージャー、クライアント、テンプレート、宣言されたリポジトリを、リアクティブな依存関係が存在するかどうかに応じて命令型またはリアクティブ型で提供します。テストスライスにはすでに @ExtendWith(SpringExtension.class) が含まれているため、JUnit 5 (JUnit Jupiter) で自動的に実行されます。
@DataNeo4jTest は、デフォルトで命令型インフラストラクチャとリアクティブインフラストラクチャの両方を提供し、暗黙的な @Transactional も追加します。ただし、Spring Test の @Transactional は、常に命令型トランザクションを意味します。宣言型トランザクションでは、命令型 PlatformTransactionManager とリアクティブ ReactiveTransactionManager のどちらが必要かを決定するメソッドの戻り値の型が必要です。
リアクティブリポジトリまたはサービスの正しいトランザクション動作をアサートするには、テストに TransactionalOperator を挿入するか、インフラストラクチャが正しいトランザクションマネージャーを選択できるようにする戻り値の型を公開するアノテーション付きメソッドを使用するサービスでドメインロジックをラップする必要があります。
テストスライスには、組み込みデータベースやその他の接続設定は組み込まれません。適切な接続を使用するかどうかはあなた次第です。
Neo4j テストコンテナーモジュール (英語) または Neo4j テストハーネスを使用する 2 つのオプションのいずれかを推奨します。Testcontainers はさまざまなサービス用のモジュールを備えた既知のプロジェクトですが、Neo4j テストハーネスはあまり知られていません。これは、Neo4j ベースの Java アプリケーションのテスト (英語) で説明されているストアドプロシージャをテストするときに特に役立つ埋め込みインスタンスです。ただし、テストハーネスはアプリケーションのテストにも使用できます。アプリケーションと同じ JVM 内でデータベースが起動されるため、パフォーマンスとタイミングが運用環境のセットアップと似ていない可能性があります。
便宜上、Neo4j テストハーネス 3.5 および 4.x/5.x および Testcontainers Neo4j の 3 つの可能なシナリオを提供します。3.5 および 4.x/5.x のバージョン間でテストハーネスが変更されたため、さまざまな例を提供します。また、4.0 には JDK 11 が必要です。
12.2.1. @DataNeo4jTest Neo4j テストハーネス 3.5 を使用する場合
リスト 41 を実行するには、次の依存関係が必要です。
<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>3.5.23</version>
<scope>test</scope>
</dependency>Neo4j 3.5 のエンタープライズバージョンの依存関係は、com.neo4j.test:neo4j-harness-enterprise および適切なリポジトリ構成で利用できます。
@DataNeo4jTest
class MovieRepositoryTest {
private static ServerControls embeddedDatabaseServer;
@BeforeAll
static void initializeNeo4j() {
embeddedDatabaseServer = TestServerBuilders.newInProcessBuilder() (1)
.newServer();
}
@AfterAll
static void stopNeo4j() {
embeddedDatabaseServer.close(); (2)
}
@DynamicPropertySource (3)
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedDatabaseServer::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> null);
}
@Test
public void findSomethingShouldWork(@Autowired Neo4jClient client) {
Optional<Long> result = client.query("MATCH (n) RETURN COUNT(n)")
.fetchAs(Long.class)
.one();
assertThat(result).hasValue(0L);
}
}
| 1 | 埋め込み Neo4j を作成するためのエントリポイント |
| 2 | これは、アプリケーションプロパティを動的に登録できるようにする Spring Boot アノテーションです。対応する Neo4j 設定を上書きします。 |
| 3 | すべてのテストが終了したら、Neo4j をシャットダウンします。 |
12.2.2. @DataNeo4jTest Neo4j テストハーネス 4.x/5.x を使用する場合
リスト 43 を実行するには、次の依存関係が必要です。
<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>4.4.16</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
</exclusion>
</exclusions>
</dependency>Neo4j 4.x/5.x のエンタープライズバージョンの依存関係は、com.neo4j.test:neo4j-harness-enterprise および適切なリポジトリ構成で利用できます。
@DataNeo4jTest
class MovieRepositoryTest {
private static Neo4j embeddedDatabaseServer;
@BeforeAll
static void initializeNeo4j() {
embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() (1)
.withDisabledServer() (2)
.build();
}
@DynamicPropertySource (3)
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedDatabaseServer::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> null);
}
@AfterAll
static void stopNeo4j() {
embeddedDatabaseServer.close(); (4)
}
@Test
public void findSomethingShouldWork(@Autowired Neo4jClient client) {
Optional<Long> result = client.query("MATCH (n) RETURN COUNT(n)")
.fetchAs(Long.class)
.one();
assertThat(result).hasValue(0L);
}
}
| 1 | 埋め込み Neo4j を作成するためのエントリポイント |
| 2 | 不要な Neo4j HTTP サーバーを無効にする |
| 3 | これは、アプリケーションプロパティを動的に登録できるようにする Spring Boot アノテーションです。対応する Neo4j 設定を上書きします。 |
| 4 | すべてのテストが終了したら、Neo4j をシャットダウンします。 |
12.2.3. @DataNeo4jTest とテストコンテナー Neo4j
もちろん、リスト 44 で示されているように、接続構成の原則は Testcontainers でも同じです。次の依存関係が必要です。
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<version>1.17.6</version>
<scope>test</scope>
</dependency>そして完全なテスト:
@DataNeo4jTest
class MovieRepositoryTCTest {
private static Neo4jContainer<?> neo4jContainer;
@BeforeAll
static void initializeNeo4j() {
neo4jContainer = new Neo4jContainer<>()
.withAdminPassword("somePassword");
neo4jContainer.start();
}
@AfterAll
static void stopNeo4j() {
neo4jContainer.close();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", neo4jContainer::getBoltUrl);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", neo4jContainer::getAdminPassword);
}
@Test
public void findSomethingShouldWork(@Autowired Neo4jClient client) {
Optional<Long> result = client.query("MATCH (n) RETURN COUNT(n)")
.fetchAs(Long.class)
.one();
assertThat(result).hasValue(0L);
}
}
12.2.4. @DynamicPropertySource の代替
上記のアノテーションがユースケースに適合しないシナリオがいくつかあります。そのうちの 1 つは、ドライバーの初期化方法を 100% で制御したい場合です。テストコンテナーを実行している場合、次のようにネストされた静的構成クラスを使用してこれを行うことができます。
@TestConfiguration(proxyBeanMethods = false)
static class TestNeo4jConfig {
@Bean
Driver driver() {
return GraphDatabase.driver(
neo4jContainer.getBoltUrl(),
AuthTokens.basic("neo4j", neo4jContainer.getAdminPassword())
);
}
}
プロパティを使用したいが @DynamicPropertySource を使用できない場合は、初期化子を使用します。
@ContextConfiguration(initializers = PriorToBoot226Test.Initializer.class)
@DataNeo4jTest
class PriorToBoot226Test {
private static Neo4jContainer<?> neo4jContainer;
@BeforeAll
static void initializeNeo4j() {
neo4jContainer = new Neo4jContainer<>()
.withAdminPassword("somePassword");
neo4jContainer.start();
}
@AfterAll
static void stopNeo4j() {
neo4jContainer.close();
}
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.neo4j.uri=" + neo4jContainer.getBoltUrl(),
"spring.neo4j.authentication.username=neo4j",
"spring.neo4j.authentication.password=" + neo4jContainer.getAdminPassword()
).applyTo(configurableApplicationContext.getEnvironment());
}
}
}
13. 監査
13.1. 基本
Spring Data は、エンティティを作成または変更したユーザーと変更がいつ発生したかを透過的に追跡するための高度なサポートを提供します。この機能を利用するには、アノテーションを使用するか、インターフェースを実装することで定義できる監査メタデータをエンティティクラスに装備する必要があります。さらに、必要なインフラストラクチャコンポーネントを登録するには、アノテーション構成または XML 構成のいずれかを介して監査を有効にする必要があります。構成サンプルについては、ストア固有のセクションを参照してください。
作成日と変更日のみを追跡するアプリケーションでは、エンティティに |
13.1.1. アノテーションベースの監査メタデータ
エンティティを作成または変更したユーザーをキャプチャーする @CreatedBy と @LastModifiedBy、および変更が発生したときにキャプチャーする @CreatedDate と @LastModifiedDate を提供します。
class Customer {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
// … further properties omitted
}
ご覧のとおり、キャプチャーする情報に応じて、アノテーションを選択的に適用できます。変更が行われたときにキャプチャーすることを示すアノテーションは、JDK8 の日付と時刻の型、long、Long、レガシー Java Date と Calendar のプロパティで使用できます。
以下のスニペットに示すように、監査メタデータは必ずしもルートレベルのエンティティに存在する必要はありませんが、埋め込まれたエンティティに追加できます(実際に使用されているストアによって異なります)。
class Customer {
private AuditMetadata auditingMetadata;
// … further properties omitted
}
class AuditMetadata {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
}
13.1.2. インターフェースベースの監査メタデータ
アノテーションを使用して監査メタデータを定義したくない場合は、ドメインクラスに Auditable インターフェースを実装させることができます。すべての監査プロパティの setter メソッドを公開します。
13.1.3. AuditorAware
@CreatedBy または @LastModifiedBy を使用する場合、監査インフラストラクチャは何らかの形で現在のプリンシパルを認識する必要があります。そのために、現在のユーザーまたはアプリケーションと対話するシステムが誰であるかをインフラストラクチャに伝えるために実装する必要がある AuditorAware<T> SPI インターフェースを提供します。ジェネリクス型 T は、@CreatedBy または @LastModifiedBy アノテーションが付けられたプロパティの型を定義します。
次の例は、Spring Security の Authentication オブジェクトを使用するインターフェースの実装を示しています。
AuditorAware の実装 class SpringSecurityAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
実装は、Spring Security が提供する Authentication オブジェクトにアクセスし、UserDetailsService 実装で作成したカスタム UserDetails インスタンスを検索します。ここでは、UserDetails 実装を介してドメインユーザーを公開しているが、見つかった Authentication に基づいて、どこからでも検索できると想定しています。
13.1.4. ReactiveAuditorAware
リアクティブインフラストラクチャを使用する場合、コンテキスト情報を利用して @CreatedBy または @LastModifiedBy 情報を提供することができます。アプリケーションと対話している現在のユーザーまたはシステムが誰であるかをインフラストラクチャに通知するために実装する必要がある ReactiveAuditorAware<T> SPI インターフェースを提供します。ジェネリクス型 T は、@CreatedBy または @LastModifiedBy でアノテーションが付けられたプロパティがどの型でなければならないかを定義します。
次の例は、リアクティブ Spring Security の Authentication オブジェクトを使用するインターフェースの実装を示しています。
ReactiveAuditorAware の実装 class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {
@Override
public Mono<User> getCurrentAuditor() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
実装は、Spring Security が提供する Authentication オブジェクトにアクセスし、UserDetailsService 実装で作成したカスタム UserDetails インスタンスを検索します。ここでは、UserDetails 実装を介してドメインユーザーを公開しているが、見つかった Authentication に基づいて、どこからでも検索できると想定しています。
よくある質問
SDN は Neo4j-OGM とどのように関係しますか ?
Neo4j-OGM (英語) はオブジェクトグラフマッピングライブラリであり、主に Spring Data Neo4j の以前のバージョンで、ノードと関連をドメインオブジェクトにマッピングするという重労働を行うバックエンドとして使用されます。現在の SDN は Neo4j-OGM を必要とせず、サポートしていません。SDN は、クラスのスキャンとメタモデルの構築にのみ Spring Data のマッピングコンテキストを使用します。
これにより、SDN が Spring エコシステムに固定されますが、CPU とメモリの使用量、特に Spring のマッピングコンテキストのすべての機能に関するフットプリントが小さいなど、いくつかの利点があります。
SDN+OGM ではなく SDN を使用する必要がある理由
SDN には、SDN+OGM にはないいくつかの機能があります。
リアクティブトランザクションを含む Springs リアクティブストーリーの完全サポート
例示による問い合わせの完全サポート
完全に不変のエンティティを完全にサポート
空間クエリを含む派生ファインダーメソッドのすべての修飾子とバリエーションのサポート
SDN は組み込み Neo4j をサポートしていますか ?
組み込み Neo4j には複数の側面があります。
SDN は埋め込みインスタンスと直接対話しますか ?
いいえ。組み込みデータベースは通常、org.neo4j.graphdb.GraphDatabaseService のインスタンスによって表され、すぐに使用できるボルトコネクターはありません。
ただし、SDN は Neo4j のテストハーネスと非常によく連携できます。テストハーネスは、実際のデータベースのドロップイン置換として特に意図されています。Neo4j 3.5, 4.x, 5.x テストハーネスのサポートは、ドライバーの Spring Boot スターター [GitHub] (英語) を介して実装されます。対応するモジュール org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure を参照してください。
どの Neo4j Java ドライバーをどのように使用できますか ?
SDN は Neo4j Java ドライバーに依存しています。各 SDN リリースでは、リリース時に利用可能な最新の Neo4j と互換性のある Neo4j Java ドライバーバージョンが使用されます。Neo4j Java Driver のパッチバージョンは通常、ドロップイン置き換えですが、SDN では、必要に応じてメソッドまたはインターフェースの変更の有無をチェックするため、マイナーバージョンであっても互換性があることを確認します。
任意の 4.x Neo4j Java ドライバーを任意の SDN 6.x バージョンで使用でき、任意の 5.x Neo4j ドライバーを任意の SDN 7.x バージョンで使用できます。
Spring Boot 付
最近では、Spring boot デプロイが Spring Data ベースのアプリケーションの デプロイである可能性が最も高くなります。Spring Boot 依存関係管理を使用して、次のようにドライバーのバージョンを変更してください。
<properties>
<neo4j-java-driver.version>5.4.0</neo4j-java-driver.version>
</properties>または
neo4j-java-driver.version = 5.4.0Spring Boot なし
Spring Boot を使用しない場合は、依存関係を手動で宣言するだけになります。Maven の場合は、次のように <dependencyManagement /> セクションを使用することをお勧めします。
<dependencyManagement>
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>5.4.0</version>
</dependency>
</dependencyManagement>Neo4j 4 は複数のデータベースをサポートしています - 使用するにはどうすればよいですか ?
データベース名を静的に構成することも、独自のデータベース名プロバイダーを実行することもできます。SDN はデータベースを作成しないことに注意してください。これは、移行ツール [GitHub] (英語) を使用して行うことも、もちろん事前に簡単なスクリプトを使用して行うこともできます。
静的に構成された
Spring Boot 構成で使用するデータベース名を次のように構成します (もちろん、同じプロパティが YML または環境ベースの構成に適用され、Spring Boot の規則が適用されます)。
spring.data.neo4j.database = yourDatabase この構成を適用すると、SDN リポジトリのすべてのインスタンス (リアクティブと命令の両方) および ReactiveNeo4jTemplate および Neo4jTemplate によって生成されたすべてのクエリが、データベース yourDatabase に対して実行されます。
動的に構成
Spring アプリケーションの型に応じて、Bean に型 Neo4jDatabaseNameProvider または ReactiveDatabaseSelectionProvider を指定します。
Bean は、たとえば Spring のセキュリティコンテキストを使用してテナントを取得できます。以下は、Spring Security で保護された命令型アプリケーションの実例です。
@Configuration
public class Neo4jConfig {
@Bean
DatabaseSelectionProvider databaseSelectionProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext()).map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated).map(Authentication::getPrincipal).map(User.class::cast)
.map(User::getUsername).map(DatabaseSelection::byName).orElseGet(DatabaseSelection::undecided);
}
}
| あるデータベースから取得したエンティティを別のデータベースと混同しないように注意してください。データベース名は新しいトランザクションごとにリクエストされるため、呼び出しの間にデータベース名を変更すると、エンティティの数が予想よりも少なくなったり、多くなったりする可能性があります。さらに悪いことに、必然的に間違ったエンティティを間違ったデータベースに保存する可能性があります。 |
Spring Boot Neo4j ヘルスインジケーターはデフォルトのデータベースをターゲットにしていますが、これを変更するにはどうすればよいですか ?
Spring Boot には、命令型とリアクティブ型の両方の Neo4j ヘルスインジケーターが付属しています。どちらのバリアントも、アプリケーションコンテキスト内で org.neo4j.driver.Driver の複数の Bean を検出でき、各インスタンスの全体的な健全性に貢献します。ただし、Neo4j ドライバーはサーバーに接続しますが、サーバー内の特定のデータベースには接続しません。Spring Boot は Spring Data Neo4j なしでドライバーを構成できます。また、どのデータベースが使用されるかという情報は Spring Data Neo4j に関連付けられているため、この情報は組み込みのヘルスインジケーターでは利用できません。
これは、多くの デプロイシナリオでは問題にならない可能性が高くなります。ただし、構成されたデータベースユーザーに少なくともデフォルトデータベースへのアクセス権がない場合、ヘルスチェックは失敗します。
これは、データベースの選択を認識しているカスタム Neo4j ヘルスコントリビューターによって軽減できます。
命令型の変形
public class DatabaseSelectionAwareNeo4jHealthIndicator extends AbstractHealthIndicator {
private final Driver driver;
private final DatabaseSelectionProvider databaseSelectionProvider;
public DatabaseSelectionAwareNeo4jHealthIndicator(
Driver driver, DatabaseSelectionProvider databaseSelectionProvider
) {
this.driver = driver;
this.databaseSelectionProvider = databaseSelectionProvider;
}
@Override
protected void doHealthCheck(Health.Builder builder) {
try {
SessionConfig sessionConfig = Optional
.ofNullable(databaseSelectionProvider.getDatabaseSelection())
.filter(databaseSelection -> databaseSelection != DatabaseSelection.undecided())
.map(DatabaseSelection::getValue)
.map(v -> SessionConfig.builder().withDatabase(v).build())
.orElseGet(SessionConfig::defaultConfig);
class Tuple {
String edition;
ResultSummary resultSummary;
Tuple(String edition, ResultSummary resultSummary) {
this.edition = edition;
this.resultSummary = resultSummary;
}
}
String query =
"CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
Tuple health = driver.session(sessionConfig)
.writeTransaction(tx -> {
Result result = tx.run(query);
String edition = result.single().get("edition").asString();
return new Tuple(edition, result.consume());
});
addHealthDetails(builder, health.edition, health.resultSummary);
} catch (Exception ex) {
builder.down().withException(ex);
}
}
static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
ServerInfo serverInfo = resultSummary.server();
builder.up()
.withDetail(
"server", serverInfo.version() + "@" + serverInfo.address())
.withDetail("edition", edition);
DatabaseInfo databaseInfo = resultSummary.database();
if (StringUtils.hasText(databaseInfo.name())) {
builder.withDetail("database", databaseInfo.name());
}
}
}
これは、利用可能なデータベースの選択を使用して、Boot が接続が正常かどうかを確認するために実行するのと同じクエリを実行します。これを適用するには、次の構成を使用します。
@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {
@Bean (1)
DatabaseSelectionAwareNeo4jHealthIndicator databaseSelectionAwareNeo4jHealthIndicator(
Driver driver, DatabaseSelectionProvider databaseSelectionProvider
) {
return new DatabaseSelectionAwareNeo4jHealthIndicator(driver, databaseSelectionProvider);
}
@Bean (2)
HealthContributor neo4jHealthIndicator(
Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators) {
return CompositeHealthContributor.fromMap(customNeo4jHealthIndicators);
}
@Bean (3)
InitializingBean healthContributorRegistryCleaner(
HealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators
) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
}
| 1 | 複数のドライバーとデータベース選択プロバイダーがある場合は、組み合わせごとに 1 つのインジケーターを作成する必要があります。 |
| 2 | これにより、これらのインジケーターがすべて Neo4j にグループ化され、デフォルトの Neo4j ヘルスインジケーターが置き換えられます。 |
| 3 | これにより、個々の投稿者が正常性エンドポイントに直接表示されなくなります。 |
リアクティブバリアント
リアクティブバリアントは基本的に同じで、リアクティブ型と対応するリアクティブインフラストラクチャクラスを使用します。
public final class DatabaseSelectionAwareNeo4jReactiveHealthIndicator
extends AbstractReactiveHealthIndicator {
private final Driver driver;
private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
public DatabaseSelectionAwareNeo4jReactiveHealthIndicator(
Driver driver,
ReactiveDatabaseSelectionProvider databaseSelectionProvider
) {
this.driver = driver;
this.databaseSelectionProvider = databaseSelectionProvider;
}
@Override
protected Mono<Health> doHealthCheck(Health.Builder builder) {
String query =
"CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
return databaseSelectionProvider.getDatabaseSelection()
.map(databaseSelection -> databaseSelection == DatabaseSelection.undecided() ?
SessionConfig.defaultConfig() :
SessionConfig.builder().withDatabase(databaseSelection.getValue()).build()
)
.flatMap(sessionConfig ->
Mono.usingWhen(
Mono.fromSupplier(() -> driver.rxSession(sessionConfig)),
s -> {
Publisher<Tuple2<String, ResultSummary>> f = s.readTransaction(tx -> {
RxResult result = tx.run(query);
return Mono.from(result.records())
.map((record) -> record.get("edition").asString())
.zipWhen((edition) -> Mono.from(result.consume()));
});
return Mono.fromDirect(f);
},
RxSession::close
)
).map((result) -> {
addHealthDetails(builder, result.getT1(), result.getT2());
return builder.build();
});
}
static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
ServerInfo serverInfo = resultSummary.server();
builder.up()
.withDetail(
"server", serverInfo.version() + "@" + serverInfo.address())
.withDetail("edition", edition);
DatabaseInfo databaseInfo = resultSummary.database();
if (StringUtils.hasText(databaseInfo.name())) {
builder.withDetail("database", databaseInfo.name());
}
}
}
そしてもちろん、設定のリアクティブ版です。Spring Boot は非リアクティブアクチュエーターエンドポイントでも使用できるように既存のリアクティブインジケーターをラップするため、2 つの異なるレジストリクリーナーが必要です。
@Configuration(proxyBeanMethods = false)
public class Neo4jHealthConfig {
@Bean
ReactiveHealthContributor neo4jHealthIndicator(
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return CompositeReactiveHealthContributor.fromMap(customNeo4jHealthIndicators);
}
@Bean
InitializingBean healthContributorRegistryCleaner(HealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
@Bean
InitializingBean reactiveHealthContributorRegistryCleaner(
ReactiveHealthContributorRegistry healthContributorRegistry,
Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
return () -> customNeo4jHealthIndicators.keySet()
.stream()
.map(HealthContributorNameFactory.INSTANCE)
.forEach(healthContributorRegistry::unregisterContributor);
}
}
Neo4j 4.4+ はさまざまなユーザーの偽装をサポートしています - どのように使用できますか ?
ユーザーの偽装は、物理的に接続されている (または技術的な) 1 人のユーザーが多数のテナントになりすますことができる大規模なマルチテナント設定で特に興味深いものです。設定によっては、必要な物理ドライバーインスタンスの数が大幅に減少します。
この機能には、サーバー側に Neo4j Enterprise 4.4+ が必要で、クライアント側に 4.4+ ドライバー (org.neo4j.driver:neo4j-java-driver:4.4.0 以降) が必要です。
命令型バージョンとリアクティブバージョンの両方で、UserSelectionProvider と ReactiveUserSelectionProvider をそれぞれ提供する必要があります。同じインスタンスを、それぞれのリアクティブバリアントである Neo4Client および Neo4jTransactionManager に渡す必要があります。
Boot レスの命令およびリアクティブ構成では、問題となる型の Bean を提供する必要があります。
public class CustomConfig {
@Bean
public UserSelectionProvider getUserSelectionProvider() {
return () -> UserSelection.impersonate("someUser");
}
}
典型的な Spring Boot シナリオでは、Boot はその機能のない SDN バージョンもサポートしているため、この機能にはもう少し作業が必要です。リスト 50 の Bean を考慮すると、クライアントとトランザクションマネージャーを完全にカスタマイズする必要があります。
public class CustomConfig {
@Bean
public Neo4jClient neo4jClient(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
UserSelectionProvider userSelectionProvider
) {
return Neo4jClient.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(userSelectionProvider)
.build();
}
@Bean
public PlatformTransactionManager transactionManager(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
UserSelectionProvider userSelectionProvider
) {
return Neo4jTransactionManager
.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(userSelectionProvider)
.build();
}
}
Spring Data Neo4j からの Neo4j クラスターインスタンスの使用
次の質問は、Neo4j AuraDB と Neo4j オンプレミスクラスターインスタンスに当てはまります。
トランザクションが Neo4j Causal Cluster でシームレスに動作するには、特定の構成が必要ですか ?
いいえ、そうではありません。SDN は、ユーザー側での構成を必要とせずに、Neo4j Causal Cluster ブックマークを内部的に使用します。同じスレッド内のトランザクション、または相互に続く同じリアクティブストリーム内のトランザクションは、予想どおり、以前に変更された値を読み取ることができます。
Neo4j クラスターに読み取り専用トランザクションを使用することは重要ですか ?
はい、そうです。Neo4j クラスターアーキテクチャは因果的クラスタリングアーキテクチャであり、プライマリサーバーとセカンダリサーバーを区別します。プライマリサーバーは、単一インスタンスまたはコアインスタンスのいずれかです。どちらも読み取りおよび書き込み操作に応答できます。書き込み操作は、コアインスタンスからクラスター内の読み取りレプリカ、より一般的にはフォロワーに伝播されます。これらのフォロワーはセカンダリサーバーです。セカンダリサーバーは書き込み操作に応答しません。
標準的な デプロイシナリオでは、クラスター内にいくつかのコアインスタンスと多数のリードレプリカが存在します。リーダーが圧倒されることがなく、クエリが読み取りレプリカにできるだけ伝播されるようにクラスターを拡張するには、操作またはクエリを読み取り専用としてマークすることが重要です。
Spring Data Neo4j も基礎となる Java ドライバーも Cypher 解析を行わず、両方のビルドブロックはデフォルトで書き込み操作を想定します。この決定は、すぐに使えるすべての操作をサポートするために行われました。スタック内の何かがデフォルトで読み取り専用であると想定されている場合、スタックは読み取りレプリカに書き込みクエリを送信して、その実行に失敗する可能性があります。
すべての findById、findAllById、findAll および定義済みの存在メソッドは、デフォルトで読み取り専用としてマークされます。 |
いくつかのオプションを以下に説明します。
@Transactional(readOnly = true)
interface PersonRepository extends Neo4jRepository<Person, Long> {
}
interface PersonRepository extends Neo4jRepository<Person, Long> {
@Transactional(readOnly = true)
Person findOneByName(String name); (1)
@Transactional(readOnly = true)
@Query("""
CALL apoc.search.nodeAll('{Person: "name",Movie: ["title","tagline"]}','contains','her')
YIELD node AS n RETURN n""")
Person findByCustomQuery(); (2)
}
| 1 | これがデフォルトで読み取り専用にならないのはなぜですか ? 上記の派生ファインダー (実際には読み取り専用であることがわかっています) では機能しますが、ユーザーがカスタム @Query を追加し、それを MERGE 構造 (もちろん書き込み操作) 経由で実装するケースをよく見かけます。 |
| 2 | カスタムプロシージャはあらゆる種類のことを行うことができますが、現時点では読み取り専用か書き込みかをここで確認する方法はありません。 |
interface PersonRepository extends Neo4jRepository<Person, Long> {
}
interface MovieRepository extends Neo4jRepository<Movie, Long> {
List<Movie> findByLikedByPersonName(String name);
}
public class PersonService {
private final PersonRepository personRepository;
private final MovieRepository movieRepository;
public PersonService(PersonRepository personRepository,
MovieRepository movieRepository) {
this.personRepository = personRepository;
this.movieRepository = movieRepository;
}
@Transactional(readOnly = true)
public Optional<PersonDetails> getPerson(Long id) { (1)
return this.repository.findById(id)
.map(person -> {
var movies = this.movieRepository
.findByLikedByPersonName(person.getName());
return new PersonDetails(person, movies);
});
}
}
| 1 | ここでは、複数のリポジトリへの複数の呼び出しが 1 つの読み取り専用トランザクションにラップされています。 |
TransactionTemplate を使用する public class PersonService {
private final TransactionTemplate readOnlyTx;
private final Neo4jClient neo4jClient;
public PersonService(PlatformTransactionManager transactionManager, Neo4jClient neo4jClient) {
this.readOnlyTx = new TransactionTemplate(transactionManager, (1)
new TransactionDefinition() {
@Override public boolean isReadOnly() {
return true;
}
}
);
this.neo4jClient = neo4jClient;
}
void internalOperation() { (2)
Collection<Node> nodes = this.readOnlyTx.execute(state -> {
return neo4jClient.query("MATCH (n) RETURN n").fetchAs(Node.class) (3)
.mappedBy((types, record) -> record.get(0).asNode())
.all();
});
}
}
| 1 | 必要な特性を備えた TransactionTemplate のインスタンスを作成します。もちろん、これはグローバル Bean にすることもできます。 |
| 2 | トランザクションテンプレートを使用する理由 1 つ目: 宣言型トランザクションは、アスペクトとプロキシで実装される性質のため、パッケージのプライベートメソッドやプライベートメソッドでは機能せず、内部メソッド呼び出し ( internalOperation を呼び出すこのサービスの別のメソッドを想像してください) でも機能しません。 |
| 3 | Neo4jClient は、SDN によって提供される固定ユーティリティです。アノテーションを付けることはできませんが、Spring と統合されます。自動マッピングやトランザクションを使用せずに、純粋なドライバーを使用して実行するすべての操作が可能になります。また、宣言的なトランザクションにも準拠します。 |
最新のブックマークを取得したり、トランザクションマネージャーをシードしたりできますか ?
ブックマーク管理で簡単に説明したように、ブックマークに関しては何も設定する必要はありません。ただし、SDN トランザクションシステムがデータベースから受信した最新のブックマークを取得すると便利な場合があります。これを行うには、BookmarkCapture のような @Bean を追加できます。
public final class BookmarkCapture
implements ApplicationListener<Neo4jBookmarksUpdatedEvent> {
@Override
public void onApplicationEvent(Neo4jBookmarksUpdatedEvent event) {
// We make sure that this event is called only once,
// the thread safe application of those bookmarks is up to your system.
Set<Bookmark> latestBookmarks = event.getBookmarks();
}
}
トランザクションシステムをシードするには、次のようなカスタマイズされたトランザクションマネージャーが必要です。
@Configuration
public class BookmarkSeedingConfig {
@Bean
public PlatformTransactionManager transactionManager(
Driver driver, DatabaseSelectionProvider databaseNameProvider) { (1)
Supplier<Set<Bookmark>> bookmarkSupplier = () -> { (2)
Bookmark a = null;
Bookmark b = null;
return Set.of(a, b);
};
Neo4jBookmarkManager bookmarkManager =
Neo4jBookmarkManager.create(bookmarkSupplier); (3)
return new Neo4jTransactionManager(
driver, databaseNameProvider, bookmarkManager); (4)
}
}
| 1 | Spring に注入させます |
| 2 | このサプライヤーには、システムに取り込みたい最新のブックマークを保持するものであれば何でもかまいません。 |
| 3 | それを使用してブックマークマネージャーを作成します |
| 4 | カスタマイズされたトランザクションマネージャーに渡します |
| アプリケーションがこのデータにアクセスしたり、このデータを提供したりする必要がない限り、上記のことを行う必要はありません。疑わしい場合は、どちらも行わないでください。 |
ブックマーク管理を無効にできますか ?
ブックマーク管理を効果的に無効にする Noop ブックマークマネージャーを提供します。
| このブックマークマネージャーは自己責任で使用してください。すべてのブックマークを削除し、ブックマークをまったく提供しないことにより、事実上ブックマーク管理が無効になります。クラスターでは、古い読み取りが発生するリスクが高くなります。単一のインスタンスでは、ほとんどの場合、何の違いも生じません。 クラスターでは、古いデータを上書きする危険がなく、古い読み取りを許容できる場合に限り、これは実用的なアプローチとなります。 |
次の構成では、関連するクラスから取得されるブックマークマネージャーの "noop" バリアントが作成されます。
@Configuration
public class BookmarksDisabledConfig {
@Bean
public Neo4jBookmarkManager neo4jBookmarkManager() {
return Neo4jBookmarkManager.noop();
}
}
Neo4jTransactionManager/Neo4jClient と ReactiveNeo4jTransactionManager/ReactiveNeo4jClient のペアを個別に構成することもできますが、特定のデータベース選択ニーズに合わせてすでに構成している場合にのみこれを行うことをお勧めします。
Neo4j 固有のアノテーションを使用する必要がありますか ?
いいえ。次の同等の Spring Data アノテーションを自由に使用できます。
| SDN 固有のアノテーション | Spring Data 共通アノテーション | 目的 | 相違 |
|---|---|---|---|
|
| アノテーション付きの属性を一意の ID としてマークします。 | 特定のアノテーションには追加機能はありません。 |
|
| クラスを永続エンティティとしてマークします。 |
|
How do I use assigned ids?
Just use @Id without @GeneratedValue and fill your id attribute via a constructor parameter or a setter or wither.
See this blog post (英語) for some general remarks about finding good ids.
How do I use externally generated ids?
We provide the interface org.springframework.data.neo4j.core.schema.IdGenerator.
Implement it in any way you want and configure your implementation like this:
@Node
public class ThingWithGeneratedId {
@Id @GeneratedValue(TestSequenceGenerator.class)
private String theId;
}
If you pass in the name of a class to @GeneratedValue, this class must have a no-args default constructor.
You can however use a string as well:
@Node
public class ThingWithIdGeneratedByBean {
@Id @GeneratedValue(generatorRef = "idGeneratingBean")
private String theId;
}
With that, idGeneratingBean refers to a bean in the Spring context.
This might be useful for sequence generating.
| Setters are not required on non-final fields for the id. |
Do I have to create repositories for each domain class?
No.
Have a look at the SDN building blocks and find the Neo4jTemplate or the ReactiveNeo4jTemplate.
Those templates know your domain and provide all necessary basic CRUD methods for retrieving, writing and counting entities.
This is our canonical movie example with the imperative template:
@DataNeo4jTest
public class TemplateExampleTest {
@Test
void shouldSaveAndReadEntities(@Autowired Neo4jTemplate neo4jTemplate) {
MovieEntity movie = new MovieEntity("The Love Bug",
"A movie that follows the adventures of Herbie, Herbie's driver, "
+ "Jim Douglas (Dean Jones), and Jim's love interest, " + "Carole Bennett (Michele Lee)");
Roles roles1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi"));
Roles roles2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi"));
movie.getActorsAndRoles().add(roles1);
movie.getActorsAndRoles().add(roles2);
neo4jTemplate.save(movie);
Optional<PersonEntity> person = neo4jTemplate.findById("Dean Jones", PersonEntity.class);
assertThat(person).map(PersonEntity::getBorn).hasValue(1931);
assertThat(neo4jTemplate.count(PersonEntity.class)).isEqualTo(2L);
}
}
And here is the reactive version, omitting the setup for brevity:
@Testcontainers
@DataNeo4jTest
class ReactiveTemplateExampleTest {
@Container private static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("org.neo4j.driver.uri", neo4jContainer::getBoltUrl);
registry.add("org.neo4j.driver.authentication.username", () -> "neo4j");
registry.add("org.neo4j.driver.authentication.password", neo4jContainer::getAdminPassword);
}
@Test
void shouldSaveAndReadEntities(@Autowired ReactiveNeo4jTemplate neo4jTemplate) {
MovieEntity movie = new MovieEntity("The Love Bug",
"A movie that follows the adventures of Herbie, Herbie's driver, Jim Douglas (Dean Jones), and Jim's love interest, Carole Bennett (Michele Lee)");
Roles role1 = new Roles(new PersonEntity(1931, "Dean Jones"), Collections.singletonList("Didi"));
Roles role2 = new Roles(new PersonEntity(1942, "Michele Lee"), Collections.singletonList("Michi"));
movie.getActorsAndRoles().add(role1);
movie.getActorsAndRoles().add(role2);
StepVerifier.create(neo4jTemplate.save(movie)).expectNextCount(1L).verifyComplete();
StepVerifier.create(neo4jTemplate.findById("Dean Jones", PersonEntity.class).map(PersonEntity::getBorn))
.expectNext(1931).verifyComplete();
StepVerifier.create(neo4jTemplate.count(PersonEntity.class)).expectNext(2L).verifyComplete();
}
}
Please note that both examples use @DataNeo4jTest from Spring Boot.
How do I use custom queries with repository methods returning Page<T> or Slice<T>?
While you don’t have to provide anything else apart a Pageable as a parameter on derived finder methods
that return a Page<T> or a Slice<T>, you must prepare your custom query to handle the pageable.
Listing 63 gives you an overview about what’s needed.
public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
Page<Person> findByName(String name, Pageable pageable); (1)
@Query(""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ "ORDER BY n.name ASC SKIP $skip LIMIT $limit"
)
Slice<Person> findSliceByName(String name, Pageable pageable); (2)
@Query(
value = ""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ "ORDER BY n.name ASC SKIP $skip LIMIT $limit",
countQuery = ""
+ "MATCH (n:Person) WHERE n.name = $name RETURN count(n)"
)
Page<Person> findPageByName(String name, Pageable pageable); (3)
}
| 1 | A derived finder method that creates a query for you.
It handles the Pageable for you.
You should use a sorted pageable. |
| 2 | This method uses @Query to define a custom query. It returns a Slice<Person>.
A slice does not know about the total number of pages, so the custom query
doesn’t need a dedicated count query. SDN will notify you that it estimates the next slice.
The Cypher template must spot both $skip and $limit Cypher parameter.
If you omit them, SDN will issue a warning. The will probably not match your expectations.
Also, the Pageable should be unsorted and you should provide a stable order.
We won’t use the sorting information from the pageable. |
| 3 | This method returns a page. A page knows about the exact number of total pages. Therefore, you must specify an additional count query. All other restrictions from the second method apply. |
Can I map named paths?
A series of connected nodes and relationships is called a "path" in Neo4j. Cypher allows paths to be named using an identifier, as exemplified by:
p = (a)-[*3..5]->(b)
or as in the infamous Movie graph, that includes the following path (in that case, one of the shortest path between two actors):
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
RETURN p
Which looks like this:
We find 3 nodes labeled Vertex and 2 nodes labeled Movie. Both can be mapped with a custom query.
Assume there’s a node entity for both Vertex and Movie as well as Actor taking care of the relationship:
@Node
public final class Person {
@Id @GeneratedValue
private final Long id;
private final String name;
private Integer born;
@Relationship("REVIEWED")
private List<Movie> reviewed = new ArrayList<>();
}
@RelationshipProperties
public final class Actor {
@RelationshipId
private final Long id;
@TargetNode
private final Person person;
private final List<String> roles;
}
@Node
public final class Movie {
@Id
private final String title;
@Property("tagline")
private final String description;
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
private final List<Actor> actors;
}
When using a query as shown in Listing 64 for a domain class of type Vertex like this
interface PeopleRepository extends Neo4jRepository<Person, Long> {
@Query(""
+ "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
+ "RETURN p"
)
List<Person> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
}
it will retrieve all people from the path and map them.
If there are relationship types on the path like REVIEWED that are also present on the domain, these
will be filled accordingly from the path.
| Take special care when you use nodes hydrated from a path based query to save data. If not all relationships are hydrated, data will be lost. |
The other way round works as well. The same query can be used with the Movie entity.
It then will only populate movies.
The following listing shows how todo this as well as how the query can be enriched with additional data
not found on the path. That data is used to correctly populate the missing relationships (in that case, all the actors)
interface MovieRepository extends Neo4jRepository<Movie, String> {
@Query(""
+ "MATCH p=shortestPath(\n"
+ "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
+ "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n"
+ "UNWIND x AS m\n"
+ "MATCH (m) <-[r:DIRECTED]-(d:Person)\n"
+ "RETURN p, collect(r), collect(d)"
)
List<Movie> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
}
The query returns the path plus all relationships and related nodes collected so that the movie entities are fully hydrated.
The path mapping works for single paths as well for multiple records of paths (which are returned by the allShortestPath function.)
| Named paths can be used efficiently to populate and return more than just a root node, see Using paths to populate and return a list of entities. |
Is @Query the only way to use custom queries?
No, @Query is not the only way to run custom queries.
The annotation is comfortable in situations in which your custom query fills your domain completely.
Please remember that SDN assumes your mapped domain model to be the truth.
That means if you use a custom query via @Query that only fills a model partially, you are in danger of using the same
object to write the data back which will eventually erase or overwrite data you didn’t consider in your query.
So, please use repositories and declarative methods with @Query in all cases where the result is shaped like your domain
model or you are sure you don’t use a partially mapped model for write commands.
What are the alternatives?
-
Projections might be already enough to shape your view on the graph: They can be used to define the depth of fetching properties and related entities in an explicit way: By modelling them.
-
If your goal is to make only the conditions of your queries dynamic, then have a look at the
QuerydslPredicateExecutorbut especially our own variant of it, theCypherdslConditionExecutor. Both mixins allow adding conditions to the full queries we create for you. Thus, you will have the domain fully populated together with custom conditions. Of course, your conditions must work with what we generate. Find the names of the root node, the related nodes and more here. -
Use the Cypher-DSL (英語) via the
CypherdslStatementExecutoror theReactiveCypherdslStatementExecutor. The Cypher-DSL is predestined to create dynamic queries. In the end, it’s what SDN uses under the hood anyway. The corresponding mixins work both with the domain type of a repository itself as well as with projections (something that the mixins for adding conditions don’t).
If you think that you can solve your problem with a partially dynamic query or a full dynamic query together with a projection, please jump back now to the chapter about Spring Data Neo4j Mixins.
Otherwise, please read up on two things: custom repository fragments the levels of abstractions we offer in SDN.
Why speaking about custom repository fragments now?
-
You might have more complex situation in which more than one dynamic query is required, but the queries still belong conceptually in a repository and not in the service layer
-
Your custom queries return a graph shaped result that fits not quite to your domain model and therefore the custom query should be accompanied by a custom mapping as well
-
You have the need for interacting with the driver, i.e. for bulk loads that should not go through object mapping.
Assume the following repository declaration that basically aggregates one base repository plus 3 fragments:
public interface MovieRepository extends Neo4jRepository<MovieEntity, String>,
DomainResults,
NonDomainResults,
LowlevelInteractions {
}
The repository contains Movies as shown in the getting started section.
The additional interface from which the repository extends (DomainResults, NonDomainResults and LowlevelInteractions)
are the fragments that addresses all the concerns above.
Using complex, dynamic custom queries but still returning domain types
The fragment DomainResults declares one additional method findMoviesAlongShortestPath:
interface DomainResults {
@Transactional(readOnly = true)
List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to);
}
This method is annotated with @Transactional(readOnly = true) to indicate that readers can answer it.
It cannot be derived by SDN but would need a custom query.
This custom query is provided by the one implementation of that interface.
The implementation has the same name with the suffix Impl:
class DomainResultsImpl implements DomainResults {
private final Neo4jTemplate neo4jTemplate; (1)
DomainResultsImpl(Neo4jTemplate neo4jTemplate) {
this.neo4jTemplate = neo4jTemplate;
}
@Override
public List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to) {
var p1 = node("Person").withProperties("name", parameter("person1"));
var p2 = node("Person").withProperties("name", parameter("person2"));
var shortestPath = shortestPath("p").definedBy(
p1.relationshipBetween(p2).unbounded()
);
var p = shortestPath.getRequiredSymbolicName();
var statement = Cypher.match(shortestPath)
.with(p, listWith(name("n"))
.in(Functions.nodes(shortestPath))
.where(anyNode().named("n").hasLabels("Movie")).returning().as("mn")
)
.unwind(name("mn")).as("m")
.with(p, name("m"))
.match(node("Person").named("d")
.relationshipTo(anyNode("m"), "DIRECTED").named("r")
)
.returning(p, Functions.collect(name("r")), Functions.collect(name("d")))
.build();
Map<String, Object> parameters = new HashMap<>();
parameters.put("person1", from.getName());
parameters.put("person2", to.getName());
return neo4jTemplate.findAll(statement, parameters, MovieEntity.class); (2)
}
}
| 1 | The Neo4jTemplate is injected by the runtime through the constructor of DomainResultsImpl. No need for @Autowired. |
| 2 | The Cypher-DSL is used to build a complex statement (pretty much the same as shown in path mapping.) The statement can be passed directly to the template. |
The template has overloads for String-based queries as well, so you could write down the query as String as well. The important takeaway here is:
-
The template "knows" your domain objects and maps them accordingly
-
@Queryis not the only option to define custom queries -
They can be generated in various ways
-
The
@Transactionalannotation is respected
Using custom queries and custom mappings
Often times a custom query indicates custom results.
Should all of those results be mapped as @Node? Of course not! Many times those objects represents read commands
and are not meant to be used as write commands.
It is also not unlikely that SDN cannot or want not map everything that is possible with Cypher.
It does however offer several hooks to run your own mapping: On the Neo4jClient.
The benefit of using the SDN Neo4jClient over the driver:
-
The
Neo4jClientis integrated with Springs transaction management -
It has a fluent API for binding parameters
-
It has a fluent API exposing both the records and the Neo4j type system so that you can access everything in your result to execute the mapping
Declaring the fragment is exactly the same as before:
interface NonDomainResults {
class Result { (1)
public final String name;
public final String typeOfRelation;
Result(String name, String typeOfRelation) {
this.name = name;
this.typeOfRelation = typeOfRelation;
}
}
@Transactional(readOnly = true)
Collection<Result> findRelationsToMovie(MovieEntity movie); (2)
}
| 1 | This is a made up non-domain result. A real world query result would probably look more complex. |
| 2 | The method this fragment adds. Again, the method is annotated with Spring’s @Transactional |
Without an implementation for that fragment, startup would fail, so here it is:
class NonDomainResultsImpl implements NonDomainResults {
private final Neo4jClient neo4jClient; (1)
NonDomainResultsImpl(Neo4jClient neo4jClient) {
this.neo4jClient = neo4jClient;
}
@Override
public Collection<Result> findRelationsToMovie(MovieEntity movie) {
return this.neo4jClient
.query(""
+ "MATCH (people:Person)-[relatedTo]-(:Movie {title: $title}) "
+ "RETURN people.name AS name, "
+ " Type(relatedTo) as typeOfRelation"
) (2)
.bind(movie.getTitle()).to("title") (3)
.fetchAs(Result.class) (4)
.mappedBy((typeSystem, record) -> new Result(record.get("name").asString(),
record.get("typeOfRelation").asString())) (5)
.all(); (6)
}
}
| 1 | Here we use the Neo4jClient, as provided by the infrastructure. |
| 2 | The client takes only in Strings, but the Cypher-DSL can still be used when rendering into a String |
| 3 | Bind one single value to a named parameter. There’s also an overload to bind a whole map of parameters |
| 4 | This is the type of the result you want |
| 5 | And finally, the mappedBy method, exposing one Record for each entry in the result plus the drivers type system if needed.
This is the API in which you hook in for your custom mappings |
The whole query runs in the context of a Spring transaction, in this case, a read-only one.
Low level interactions
Sometimes you might want to do bulk loadings from a repository or delete whole subgraphs or interact in very specific ways with the Neo4j Java-Driver. This is possible as well. The following example shows how:
interface LowlevelInteractions {
int deleteGraph();
}
class LowlevelInteractionsImpl implements LowlevelInteractions {
private final Driver driver; (1)
LowlevelInteractionsImpl(Driver driver) {
this.driver = driver;
}
@Override
public int deleteGraph() {
try (Session session = driver.session()) {
SummaryCounters counters = session
.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()) (2)
.counters();
return counters.nodesDeleted() + counters.relationshipsDeleted();
}
}
}
| 1 | Work with the driver directly. As with all the examples: There is no need for @Autowired magic. All the fragments
are actually testable on their own. |
| 2 | The use case is made up. Here we use a driver managed transaction deleting the whole graph and return the number of deleted nodes and relationships |
This interaction does of course not run in a Spring transaction, as the driver does not know about Spring.
Putting it all together, this test succeeds:
@Test
void customRepositoryFragmentsShouldWork(
@Autowired PersonRepository people,
@Autowired MovieRepository movies
) {
PersonEntity meg = people.findById("Meg Ryan").get();
PersonEntity kevin = people.findById("Kevin Bacon").get();
List<MovieEntity> moviesBetweenMegAndKevin = movies.
findMoviesAlongShortestPath(meg, kevin);
assertThat(moviesBetweenMegAndKevin).isNotEmpty();
Collection<NonDomainResults.Result> relatedPeople = movies
.findRelationsToMovie(moviesBetweenMegAndKevin.get(0));
assertThat(relatedPeople).isNotEmpty();
assertThat(movies.deleteGraph()).isGreaterThan(0);
assertThat(movies.findAll()).isEmpty();
assertThat(people.findAll()).isEmpty();
}
As a final word: All three interfaces and implementations are picked up by Spring Data Neo4j automatically. There is no need for further configuration. Also, the same overall repository could have been created with only one additional fragment (the interface defining all three methods) and one implementation. The implementation would than have had all three abstractions injected (template, client and driver).
All of this applies of course to reactive repositories as well.
They would work with the ReactiveNeo4jTemplate and ReactiveNeo4jClient and the reactive session provided by the driver.
If you have recurring methods for all repositories, you could swap out the default repository implementation.
How do I use custom Spring Data Neo4j base repositories?
Basically the same ways as the shared Spring Data Commons documentation shows for Spring Data JPA in Section 10.6.2. Only that in our case you would extend from
public class MyRepositoryImpl<T, ID> extends SimpleNeo4jRepository<T, ID> {
MyRepositoryImpl(
Neo4jOperations neo4jOperations,
Neo4jEntityInformation<T, ID> entityInformation
) {
super(neo4jOperations, entityInformation); (1)
}
@Override
public List<T> findAll() {
throw new UnsupportedOperationException("This implementation does not support `findAll`");
}
}
| 1 | This signature is required by the base class. Take the Neo4jOperations (the actual specification of the Neo4jTemplate)
and the entity information and store them on an attribute if needed. |
In this example we forbid the use of the findAll method.
You could add methods taking in a fetch depth and run custom queries based on that depth.
One way to do this is shown in Listing 67.
To enable this base repository for all declared repositories enable Neo4j repositories with: @EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class).
How do I audit entities?
All Spring Data annotations are supported. Those are
-
org.springframework.data.annotation.CreatedBy org.springframework.data.annotation.CreatedDateorg.springframework.data.annotation.LastModifiedByorg.springframework.data.annotation.LastModifiedDate
第 13 章は、Spring Data Commons のより大きなコンテキストで監査を使用する方法の概要を示します。次のリストは、Spring Data Neo4j によって提供されるすべての構成オプションを示しています。
@Configuration
@EnableNeo4jAuditing(
modifyOnCreate = false, (1)
auditorAwareRef = "auditorProvider", (2)
dateTimeProviderRef = "fixedDateTimeProvider" (3)
)
class AuditingConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of("A user");
}
@Bean
public DateTimeProvider fixedDateTimeProvider() {
return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE);
}
}
| 1 | 作成時に変更データも書き込む場合は true に設定します |
| 2 | この属性を使用して、監査を提供する Bean の名前を指定します。(つまりユーザー名) |
| 3 | この属性を使用して、現在の日付を提供する Bean の名前を指定します。この場合、上記の構成はテストの一部であるため、固定の日付が使用されます。 |
リアクティブバージョンは、監査者認識 Bean の型が ReactiveAuditorAware であることを除けば基本的に同じであるため、監査者の取得はリアクティブフローの一部です。
これらの監査メカニズムに加えて、BeforeBindCallback<T> または ReactiveBeforeBindCallback<T> を実装する Bean をいくつでもコンテキストに追加できます。これらの Bean は Spring Data Neo4j によって取得され、エンティティが永続化される直前に (Ordered を実装している場合、または @Order のアノテーションが付けられている場合) 順番に呼び出されます。
エンティティを変更することも、完全に新しいエンティティを返すこともできます。次の例では、エンティティが永続化される前に 1 つの属性を変更する 1 つのコールバックをコンテキストに追加します。
@Configuration
class CallbacksConfig {
@Bean
BeforeBindCallback<ThingWithAssignedId> nameChanger() {
return entity -> {
ThingWithAssignedId updatedThing = new ThingWithAssignedId(
entity.getTheId(), entity.getName() + " (Edited)");
return updatedThing;
};
}
@Bean
AfterConvertCallback<ThingWithAssignedId> randomValueAssigner() {
return (entity, definition, source) -> {
entity.setRandomValue(UUID.randomUUID().toString());
return entity;
};
}
}
追加の構成は必要ありません。
「例で検索」を使用するにはどうすればよいですか ?
「例による検索」は SDN の新機能です。エンティティをインスタンス化するか、既存のエンティティを使用します。このインスタンスを使用して、org.springframework.data.domain.Example を作成します。リポジトリが org.springframework.data.neo4j.repository.Neo4jRepository または org.springframework.data.neo4j.repository.ReactiveNeo4jRepository を継承している場合は、リスト 76 に示すように、例を使用して利用可能な findBy メソッドをすぐに使用できます。
Example<MovieEntity> movieExample = Example.of(new MovieEntity("The Matrix", null));
Flux<MovieEntity> movies = this.movieRepository.findAll(movieExample);
movieExample = Example.of(
new MovieEntity("Matrix", null),
ExampleMatcher
.matchingAny()
.withMatcher(
"title",
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
)
);
movies = this.movieRepository.findAll(movieExample);
個々のプロパティを無効にすることもできます。これにより、適切な NOT 操作が追加され、= が <> に変わります。すべてのスカラーデータ型とすべての文字列演算子がサポートされています。
Example<MovieEntity> movieExample = Example.of(
new MovieEntity("Matrix", null),
ExampleMatcher
.matchingAny()
.withMatcher(
"title",
ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
)
.withTransformer("title", Neo4jPropertyValueTransformers.notMatching())
);
Flux<MovieEntity> allMoviesThatNotContainMatrix = this.movieRepository.findAll(movieExample);
Spring Data Neo4j を使用するには Spring Boot が必要ですか ?
いいえ、そうではありません。Spring Boot を介した Spring の多くの側面の自動構成は多くの手動作業を軽減し、新しい Spring プロジェクトをセットアップする場合に推奨されるアプローチですが、これを使用する必要はありません。
上記のソリューションには次の依存関係が必要です。
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>7.0.1</version>
</dependency>Gradle セットアップのコーディネートも同じです。
静的または動的に別のデータベースを選択するには、Neo4j 4 は複数のデータベースをサポートしています - どのように使用すればよいですか ? に従って、型 DatabaseSelectionProvider の Bean を追加します。リアクティブシナリオの場合は、ReactiveDatabaseSelectionProvider を提供します。
Spring Boot を使用せずに Spring コンテキスト内で Spring Data Neo4j を使用する
必要な Bean の導入をサポートするために、2 つの抽象構成クラスが提供されています。つまり、命令型データベースアクセス用の org.springframework.data.neo4j.config.AbstractNeo4jConfig とリアクティブバージョン用の org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig です。これらはそれぞれ @EnableNeo4jRepositories および @EnableReactiveNeo4jRepositories で使用することを目的としています。使用例については、リスト 78 およびリスト 79 を参照してください。どちらのクラスでも、ドライバーを作成する必要がある driver() をオーバーライドする必要があります。
Neo4j クライアントの命令型バージョン、テンプレートおよび命令型リポジトリのサポートを取得するには、ここに示すようなものを使用します。
@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractNeo4jConfig {
@Override @Bean
public Driver driver() { (1)
return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
@Override
protected Collection<String> getMappingBasePackages() {
return Collections.singletonList(Person.class.getPackage().getName());
}
@Override @Bean (2)
protected DatabaseSelectionProvider databaseSelectionProvider() {
return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
}
}
| 1 | ドライバー Bean が必要です。 |
| 2 | これは、yourDatabase という名前のデータベースを静的に選択します。これはオプションです。 |
次のリストは、リアクティブな Neo4j クライアントとテンプレートを提供し、リアクティブなトランザクション管理を有効にし、Neo4j 関連のリポジトリを検出します。
@Configuration
@EnableReactiveNeo4jRepositories
@EnableTransactionManagement
class MyConfiguration extends AbstractReactiveNeo4jConfig {
@Bean
@Override
public Driver driver() {
return GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
@Override
protected Collection<String> getMappingBasePackages() {
return Collections.singletonList(Person.class.getPackage().getName());
}
}
CDI 2.0 環境での Spring Data Neo4j の使用
便宜上、Neo4jCdiExtension による CDI 拡張機能を提供します。互換性のある CDI 2.0 コンテナーで実行すると、Java のサービスローダー SPI [Oracle] (英語) を通じて自動的に登録およびロードされます。
アプリケーションに導入する必要があるのは、Neo4j Java Driver を生成するアノテーション付き型だけです。
public class Neo4jConfig {
@Produces @ApplicationScoped
public Driver driver() { (1)
return GraphDatabase
.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "secret"));
}
public void close(@Disposes Driver driver) {
driver.close();
}
@Produces @Singleton
public DatabaseSelectionProvider getDatabaseSelectionProvider() { (2)
return DatabaseSelectionProvider.createStaticDatabaseSelectionProvider("yourDatabase");
}
}
| 1 | リスト 78 のプレーン Spring と同じですが、対応する CDI インフラストラクチャのアノテーションが付けられます。 |
| 2 | これはオプションです。ただし、カスタムデータベース選択プロバイダーを実行する場合は、この Bean を修飾してはなりません。 |
たとえば Weld (英語) が提供するような SE コンテナーで実行している場合は、次のように拡張機能を有効にできます。
public class SomeClass {
void someMethod() {
try (SeContainer container = SeContainerInitializer.newInstance()
.disableDiscovery()
.addExtensions(Neo4jCdiExtension.class)
.addBeanClasses(YourDriverFactory.class)
.addPackages(Package.getPackage("your.domain.package"))
.initialize()
) {
SomeRepository someRepository = container.select(SomeRepository.class).get();
}
}
}
付録
付録 A: Spring Data Neo4j
変換
組み込みの変換
すぐに使える幅広い変換をサポートします。公式ドライバーマニュアルでサポートされている暗号型のリストを見つけてください: Cypher 値の操作 (英語) 。
ラッパー型のプリミティブ型も同様にサポートされます。
| ドメインの種類 | 暗号の種類 | ネイティブ型に直接マッピングします |
|---|---|---|
| Boolean | ✔ |
| ブール値のリスト | ✔ |
| 整数 | ✔ |
| 整数のリスト | ✔ |
| フロート | ✔ |
| String | ✔ |
| 文字列のリスト | ✔ |
| ByteArray | ✔ |
| 長さ 1 の ByteArray | |
| 長さ 1 の文字列 | |
| 長さ 1 の文字列のリスト | |
| ISO 8601 日付 ( | |
| フロート一覧 | ✔ |
| String | |
| 文字列のリスト | |
| 整数 | |
| 整数のリスト | |
| BCP 47 言語タグとしてフォーマットされた文字列 | |
| 整数 | |
| 整数のリスト | |
| String | |
| String | |
| 日付 | ✔ |
| 時間 | ✔ |
| LocalTime | ✔ |
| 日時 | ✔ |
| LocalDateTime | ✔ |
| 持続時間 | |
| 持続時間 | |
| 持続時間 | ✔ |
| ポイント | ✔ |
| CRS 4326 のポイント | |
| CRS 4979 のポイント | |
| CRS 7203 のポイント | |
| CRS 9157 のポイント | |
| CRS 4326 と緯度 / 経度に対応する x/y を持つポイント | |
| String (列挙型の名前の値) | |
| 文字列のリスト (列挙型の名前の値) | |
java.net.URL | String | |
java.net.URI | String |
カスタム変換
特定の型の属性の場合
エンティティ内で独自の型を操作する場合、または @Query アノテーション付きメソッドのパラメーターとして独自の型を操作する場合は、カスタムコンバーター実装を定義して提供できます。まず、GenericConverter を実装し、コンバーターが処理する必要がある型を登録する必要があります。エンティティプロパティの型コンバーターの場合、型を Neo4j Java Driver Value との間で変換する必要があります。コンバーターがリポジトリ内のカスタムクエリメソッドでのみ動作することを想定している場合は、Value 型への一方向変換を提供するだけで十分です。
public class MyCustomTypeConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<ConvertiblePair> convertiblePairs = new HashSet<>();
convertiblePairs.add(new ConvertiblePair(MyCustomType.class, Value.class));
convertiblePairs.add(new ConvertiblePair(Value.class, MyCustomType.class));
return convertiblePairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (MyCustomType.class.isAssignableFrom(sourceType.getType())) {
// convert to Neo4j Driver Value
return convertToNeo4jValue(source);
} else {
// convert to MyCustomType
return convertToMyCustomType(source);
}
}
}
SDN にコンバーターを認識させるには、コンバーターを Neo4jConversions に登録する必要があります。これを行うには、org.springframework.data.neo4j.core.convert.Neo4jConversions 型の @Bean を作成する必要があります。それ以外の場合、Neo4jConversions は内部のデフォルトコンバーターのみを使用してバックグラウンドで作成されます。
@Bean
public Neo4jConversions neo4jConversions() {
Set<GenericConverter> additionalConverters = Collections.singleton(new MyCustomTypeConverter());
return new Neo4jConversions(additionalConverters);
}
アプリケーションで複数のコンバーターが必要な場合は、Neo4jConversions コンストラクターに必要な数だけコンバーターを追加できます。
特定の属性のみ
一部の特定の属性のみの変換が必要な場合は、@ConvertWith が提供されます。これは、エンティティ (@Node) と関連プロパティ (@RelationshipProperties) の両方の属性に付けることができるアノテーションです。これは、converter 属性を介して Neo4jPersistentPropertyConverter を定義し、前者を構築するオプションの Neo4jPersistentPropertyConverterFactory を定義します。Neo4jPersistentPropertyConverter の実装により、特定の型のすべての特定の変換に対処できます。さらに、@ConvertWith は、Neo4jPersistentPropertyConverter を実装するアプリケーションコンテキストで Spring Bean を参照するための converterRef も提供します。新しいコンバーターを構築するよりも、参照されている Bean が優先されます。
ネイティブ型を使用しない Neo4j-OGM スキームとの下位互換性のために、メタアノテーション付きアノテーションとして @DateLong および @DateString を提供します。これらは、上記の概念に基づいて構築されたメタアノテーション付きのアノテーションです。
複合プロパティ
@CompositeProperty を使用すると、型 Map<String, Object> または Map<? extends Enum, Object> の属性を複合プロパティとして保存できます。マップ内のすべてのエントリは、プロパティを含むノードまたはリレーションシップにプロパティとして追加されます。構成済みの接頭辞を付けるか、プロパティの名前を接頭辞として付けます。この機能はすぐに使用できるマップに対してのみ提供されますが、Neo4jPersistentPropertyToMapConverter を、@CompositeProperty で使用するコンバーターとして構成することができます。Neo4jPersistentPropertyToMapConverter は、特定の型をマップに分解し、マップから再構成する方法を知る必要があります。
Neo4jClient
Spring Data Neo4j には Neo4j クライアントが付属しており、Neo4j の Java ドライバーの上に薄い層を提供します。
プレーン Java ドライバー [GitHub] (英語) は、命令型およびリアクティブバージョンに加えて非同期 API を提供する非常に多用途のツールですが、Spring アプリケーションレベルのトランザクションとは統合されていません。
SDN は、慣用的なクライアントの概念を通じてドライバーを可能な限り直接的に使用します。
クライアントには次の主なゴールがあります
命令型シナリオとリアクティブシナリオの両方に対応する Springs トランザクション管理への統合
必要に応じて JTA トランザクションに参加します
命令型シナリオとリアクティブ型シナリオの両方に一貫した API を提供する
マッピングのオーバーヘッドを追加しない
SDN はこれらすべての機能に依存し、使用してエンティティマッピング機能を実現します。
命令型とリアクティブの Neo4 クライアントがスタック内のどこに配置されているかについては、SDN の構成要素を参照してください。
Neo4j クライアントには 2 つの種類があります。
org.springframework.data.neo4j.core.Neo4jClientorg.springframework.data.neo4j.core.ReactiveNeo4jClient
どちらのバージョンも同じ語彙と構文を使用する API を提供しますが、API 互換性はありません。どちらのバージョンも、クエリを指定し、パラメーターをバインドし、結果を抽出するための同じ流れるような API を備えています。
命令的ですか、それともリアクティブ的ですか ?
Neo4j クライアントとの対話は通常、次の呼び出しで終了します。
fetch().one()fetch().first()fetch().all()run()
命令型バージョンはこの時点でデータベースと対話し、リクエストされた結果または要約を Optional<> または Collection でラップして取得します。
対照的に、リアクティブバージョンは、リクエストされた型のパブリッシャーを返します。データベースとの対話と結果の取得は、パブリッシャーがサブスクライブされるまで行われません。パブリッシャーは 1 回だけサブスクライブできます。
クライアントのインスタンスを取得する
SDN のほとんどの場合と同様、両方のクライアントは構成されたドライバーインスタンスに依存します。
public class Demo {
public static void main(String...args) {
Driver driver = GraphDatabase
.driver("neo4j://localhost:7687", AuthTokens.basic("neo4j", "secret"));
Neo4jClient client = Neo4jClient.create(driver);
}
}
ドライバーは、4.0 データベースに対してのみリアクティブセッションを開くことができ、それより低いバージョンでは例外が発生して失敗します。
public class Demo {
public static void main(String...args) {
Driver driver = GraphDatabase
.driver("neo4j://localhost:7687", AuthTokens.basic("neo4j", "secret"));
ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver);
}
}
トランザクションを有効にした場合に備えて、Neo4jTransactionManager または ReactiveNeo4jTransactionManager の提供に使用したものと同じドライバーインスタンスをクライアントに使用してください。ドライバーの別のインスタンスを使用する場合、クライアントはトランザクションを同期できません。 |
当社の Spring Boot スターターは、環境 (命令型またはリアクティブ) に適合する Neo4j クライアントのすぐに使用できる Bean を提供しており、通常は独自のインスタンスを構成する必要はありません。
使用方法
ターゲットデータベースの選択
Neo4j クライアントは、Neo4j 4.0 のマルチデータベース機能とともに使用できるように十分に準備されています。特に指定しない限り、クライアントはデフォルトのデータベースを使用します。クライアントの流れるような API により、実行するクエリの宣言後にターゲットデータベースを 1 回だけ指定できます。リスト 86 は、リアクティブクライアントを使用してそれを示します。
Flux<Map<String, Object>> allActors = client
.query("MATCH (p:Person) RETURN p")
.in("neo4j") (1)
.fetch()
.all();
| 1 | クエリを実行するターゲットデータベースを選択します。 |
クエリの指定
クライアントとの対話はクエリから始まります。クエリはプレーン String または Supplier<String> によって定義できます。サプライヤーはできるだけ遅く評価され、任意のクエリビルダーによって提供できます。
Mono<Map<String, Object>> firstActor = client
.query(() -> "MATCH (p:Person) RETURN p")
.fetch()
.first();
結果の取得
前のリストが示すように、クライアントとの対話は常に fetch の呼び出しと受信される結果の数で終了します。リアクティブ型と強制型のクライアントオファーの両方
one()クエリからは 1 つの結果だけが期待されます
first()結果を期待して最初のレコードを返します
all()返されたすべてのレコードを取得します
命令型クライアントはそれぞれ Optional<T> と Collection<T> を返しますが、リアクティブクライアントは Mono<T> と Flux<T> を返し、後者はサブスクライブされている場合にのみ実行されます。
クエリからの結果が期待できない場合は、クエリを指定した後に run() を使用します。
Mono<ResultSummary> summary = reactiveClient
.query("MATCH (m:Movie) where m.title = 'Aeon Flux' DETACH DELETE m")
.run();
summary
.map(ResultSummary::counters)
.subscribe(counters ->
System.out.println(counters.nodesDeleted() + " nodes have been deleted")
); (1)
| 1 | 実際のクエリは、パブリッシャーをサブスクライブすることによってここでトリガーされます。 |
少し時間を取って両方のリストを比較し、実際のクエリがトリガーされたときの違いを理解しましょう。
ResultSummary resultSummary = imperativeClient
.query("MATCH (m:Movie) where m.title = 'Aeon Flux' DETACH DELETE m")
.run(); (1)
SummaryCounters counters = resultSummary.counters();
System.out.println(counters.nodesDeleted() + " nodes have been deleted")
| 1 | ここでは、クエリがすぐにトリガーされます。 |
マッピングパラメーター
クエリには名前付きパラメーター ($someName) を含めることができ、Neo4j クライアントを使用すると値をそれらに簡単にバインドできます。
| クライアントは、すべてのパラメーターがバインドされているかどうか、値が多すぎるかどうかをチェックしません。それはドライバーに任せられます。ただし、クライアントはパラメーター名を 2 回使用することを禁止します。 |
Java ドライバーが変換せずに理解できる単純な型をバインドすることも、複雑なクラスをバインドすることもできます。複雑なクラスの場合は、このリストに示すようにバインダー関数を提供する必要があります。どの単純な型がサポートされているかを確認するには、ドライバーマニュアル (英語) を参照してください。
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", "Li.*");
Flux<Map<String, Object>> directorAndMovies = client
.query(
"MATCH (p:Person) - [:DIRECTED] -> (m:Movie {title: $title}), (p) - [:WROTE] -> (om:Movie) " +
"WHERE p.name =~ $name " +
" AND p.born < $someDate.year " +
"RETURN p, om"
)
.bind("The Matrix").to("title") (1)
.bind(LocalDate.of(1979, 9, 21)).to("someDate")
.bindAll(parameters) (2)
.fetch()
.all();
| 1 | 単純な型をバインドするための流れるような API があります。 |
| 2 | あるいは、名前付きパラメーターのマップを介してパラメーターをバインドすることもできます。 |
SDN は多くの複雑なマッピングを実行し、クライアントから使用できるものと同じ API を使用します。
リスト 91 の自転車の所有者などの特定のドメインオブジェクトの Function<T, Map<String, Object>> を Neo4j クライアントに提供して、それらのドメインオブジェクトをドライバーが理解できるパラメーターにマッピングできます。
public class Director {
private final String name;
private final List<Movie> movies;
Director(String name, List<Movie> movies) {
this.name = name;
this.movies = new ArrayList<>(movies);
}
public String getName() {
return name;
}
public List<Movie> getMovies() {
return Collections.unmodifiableList(movies);
}
}
public class Movie {
private final String title;
public Movie(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
マッピング関数は、リスト 92 が示すように、クエリ内で発生する可能性のあるすべての名前付きパラメーターを入力する必要があります。
Director joseph = new Director("Joseph Kosinski",
Arrays.asList(new Movie("Tron Legacy"), new Movie("Top Gun: Maverick")));
Mono<ResultSummary> summary = client
.query(""
+ "MERGE (p:Person {name: $name}) "
+ "WITH p UNWIND $movies as movie "
+ "MERGE (m:Movie {title: movie}) "
+ "MERGE (p) - [o:DIRECTED] -> (m) "
)
.bind(joseph).with(director -> { (1)
Map<String, Object> mappedValues = new HashMap<>();
List<String> movies = director.getMovies().stream()
.map(Movie::getTitle).collect(Collectors.toList());
mappedValues.put("name", director.getName());
mappedValues.put("movies", movies);
return mappedValues;
})
.run();
| 1 | with メソッドでは、バインダー機能を指定できます。 |
結果オブジェクトの操作
どちらのクライアントもマップのコレクションまたは発行者 (Map<String, Object>) を返します。これらのマップは、クエリによって生成された可能性のあるレコードと正確に一致します。
さらに、独自の BiFunction<TypeSystem, Record, T> から fetchAs をプラグインして、ドメインオブジェクトを再現できます。
Mono<Director> lily = client
.query(""
+ " MATCH (p:Person {name: $name}) - [:DIRECTED] -> (m:Movie)"
+ "RETURN p, collect(m) as movies")
.bind("Lilly Wachowski").to("name")
.fetchAs(Director.class).mappedBy((TypeSystem t, Record record) -> {
List<Movie> movies = record.get("movies")
.asList(v -> new Movie((v.get("title").asString())));
return new Director(record.get("name").asString(), movies);
})
.one();
TypeSystem は、基礎となる Java ドライバーがレコードを埋めるために使用した型へのアクセスを提供します。
ドメイン対応マッピング関数の使用
クエリの結果にアプリケーション内のエンティティ定義を持つノードが含まれることがわかっている場合は、注入可能な MappingContext を使用してマッピング関数を取得し、マッピング中にそれらの関数を適用できます。
BiFunction<TypeSystem, MapAccessor, Movie> mappingFunction = neo4jMappingContext.getRequiredMappingFunctionFor(Movie.class);
Mono<Director> lily = client
.query(""
+ " MATCH (p:Person {name: $name}) - [:DIRECTED] -> (m:Movie)"
+ "RETURN p, collect(m) as movies")
.bind("Lilly Wachowski").to("name")
.fetchAs(Director.class).mappedBy((TypeSystem t, Record record) -> {
List<Movie> movies = record.get("movies")
.asList(movie -> mappingFunction.apply(t, movie));
return new Director(record.get("name").asString(), movies);
})
.one();
マネージドトランザクションの使用中にドライバーと直接対話する
Neo4jClient または ReactiveNeo4jClient の独自の「クライアント」アプローチを望まない場合、または気に入らない場合は、データベースとのすべての対話をクライアントにコードに委譲させることができます。委譲後の対話は、クライアントの命令型バージョンとリアクティブ型バージョンでは若干異なります。
命令型バージョンは、コールバックとして Function<StatementRunner, Optional<T>> を受け取ります。空のオプションを返しても問題ありません。
StatementRunner に委譲する Optional<Long> result = client
.delegateTo((StatementRunner runner) -> {
// Do as many interactions as you want
long numberOfNodes = runner.run("MATCH (n) RETURN count(n) as cnt")
.single().get("cnt").asLong();
return Optional.of(numberOfNodes);
})
// .in("aDatabase") (1)
.run();
| 1 | ターゲットデータベースの選択で説明されているデータベースの選択はオプションです。 |
リアクティブバージョンは RxStatementRunner を受け取ります。
RxStatementRunner に委譲する Mono<Integer> result = client
.delegateTo((RxStatementRunner runner) ->
Mono.from(runner.run("MATCH (n:Unused) DELETE n").summary())
.map(ResultSummary::counters)
.map(SummaryCounters::nodesDeleted))
// .in("aDatabase") (1)
.run();
| 1 | ターゲットデータベースのオプションの選択。 |
クエリの作成
この章では、SDN の抽象化レイヤーを使用する場合のクエリの技術的な作成について説明します。考えられるすべてのケースについて説明するわけではなく、その背後にある一般的な考え方に固執するため、いくつかの簡略化が行われます。
保存
find/load 操作に加えて、save 操作はデータを操作するときに最もよく使用される操作の 1 つです。通常、保存操作呼び出しでは、データベースに対して複数のステートメントを発行して、結果のグラフモデルが指定された Java モデルと一致することを確認します。
ノードの識別子が見つからない場合はノードを作成し、ノード自体が存在する場合はノードのプロパティを更新する Union ステートメントが作成されます。
(
OPTIONAL MATCH (hlp:Person) WHERE id(hlp) = $__id__ WITH hlp WHERE hlp IS NULL CREATE (n:Person) SET n = $__properties__ RETURN id(n) UNION MATCH (n) WHERE id(n) = $__id__ SET n = $__properties__ RETURN id(n))エンティティが新しくない場合、ドメインモデルで最初に見つかった型のすべての関連がデータベースから削除されます。
(
MATCH (startNode)-[rel:Has]→(:Hobby) WHERE id(startNode) = $fromId DELETE rel)関連エンティティはルートエンティティと同じ方法で作成されます。
(
OPTIONAL MATCH (hlp:Hobby) WHERE id(hlp) = $__id__ WITH hlp WHERE hlp IS NULL CREATE (n:Hobby) SET n = $__properties__ RETURN id(n) UNION MATCH (n) WHERE id(n) = $__id__ SET n = $__properties__ RETURN id(n))関連そのものが生まれる
(
MATCH (startNode) WHERE id(startNode) = $fromId MATCH (endNode) WHERE id(endNode) = 631 MERGE (startNode)-[:Has]→(endNode))関連エンティティが他のエンティティとの関連も持っている場合は、2. と同じ手順が開始されます。
ルートエンティティで次に定義される関連は 2 から始まりますが、first を next に置き換えます。
| ご覧のとおり、SDN はグラフモデルと Java の世界の同期を保つために最善を尽くしています。これが、データベースからリレーションシップが削除される可能性があるため、サブグラフのロード、操作、保存を行わないことを強くお勧めする理由の 1 つです。 |
複数のエンティティ
save 操作は、同じ型の複数のエンティティを受け入れる機能でオーバーロードされています。生成された ID 値を使用する場合、またはオプティミスティックロックを使用する場合、すべてのエンティティで個別の CREATE 呼び出しが行われます。
他の場合には、SDN はエンティティ情報を含むパラメーターリストを作成し、MERGE 呼び出しでそれを提供します。
UNWIND $__entities__ AS entity MERGE (n:Person {customId: entity.$__id__}) SET n = entity.__properties__ RETURN collect(n.customId) AS $__ids__
パラメーターは次のようになります
:params {__entities__: [{__id__: 'aa', __properties__: {name: "PersonName", theId: "aa"}}, {__id__ 'bb', __properties__: {name: "AnotherPersonName", theId: "bb"}}]}
ロード
load ドキュメントでは、クエリの MATCH 部分がどのように見えるかだけでなく、データがどのように返されるかについても説明します。
最も単純なロード操作は findById 呼び出しです。これは、クエリした型のラベルを持つすべてのノードを照合し、ID 値に対してフィルターを実行します。
MATCH (n:Person) WHERE id(n) = 1364
提供されたカスタム ID がある場合、SDN は定義したプロパティを ID として使用します。
MATCH (n:Person) WHERE n.customId = 'anId'
返されるデータは地図射影 (英語) として定義されます。
RETURN n{.first_name, .personNumber, __internalNeo4jId__: id(n), __nodeLabels__: labels(n)}
ご覧のとおり、そこには __internalNeo4jId__ と __nodeLabels__ という 2 つの特別なフィールドがあります。データを Java オブジェクトにマッピングする場合、どちらも重要です。__internalNeo4jId__ の値は id(n) または指定されたカスタム ID のいずれかですが、マッピングプロセスでは参照する既知のフィールドが 1 つ存在する必要があります。__nodeLabels__ は、このノード上で定義されているラベルをすべて見つけてマッピングできることを保証します。これは、継承が使用され、具象クラスに対してクエリを実行しない場合、またはスーパー型のみを定義する関連が定義されている場合に必要です。
人間関連について話す: エンティティ内でリレーションシップを定義している場合、パターン内包表記 (英語) として返されたマップに追加されます。上記の戻り部分は次のようになります。
RETURN n{.first_name, …, Person_Has_Hobby: [(n)-[:Has]→(n_hobbies:Hobby)|n_hobbies{__internalNeo4jId__: id(n_hobbies), .name, nodeLabels: labels(n_hobbies)}]}
SDN で使用されるマップ射影とパターン理解により、定義したプロパティと関連のみがクエリされることが保証されます。
自己参照ノードがある場合、または返されるデータにサイクルが発生する可能性のあるスキーマを作成している場合、SDN はカスケード / データ駆動型のクエリ作成にフォールバックします。特定のノードを検索する最初のクエリから開始して条件を考慮し、結果のノードをステップ順に調べ、それらの関連もマップされている場合は、その場でさらにクエリを作成します。このクエリの作成と実行のループは、クエリで新しい関連やノードが見つからなくなるまで継続します。作成方法は、保存 / 更新プロセスに似ています。
カスタムクエリ
Spring Data Neo4j では、他のすべての Spring Data モジュールと同様に、リポジトリ内でカスタムクエリを指定できます。これらは、派生クエリ関数を介してファインダーロジックを表現できない場合に便利です。
Spring Data Neo4j は内部で非常にレコード指向で動作するため、この点に留意し、同じ「ルートノード」の複数のレコードを含む結果セットを構築しないことが重要です。
リポジトリからカスタムクエリを使用する別の形式、特にカスタムマッピングでカスタムクエリを使用する方法については、FAQ も参照してください: カスタムクエリを使用する唯一の方法は @Query ですか ? 。 |
リレーションシップを伴うクエリ
デカルト積に注意してください
MATCH (m:Movie{title: 'The Matrix'})←[r:ACTED_IN]-(p:Person) return m,r,p のようなクエリがあり、次のような結果になると仮定します。
+------------------------------------------------------------------------------------------+
| m | r | p |
+------------------------------------------------------------------------------------------+
| (:Movie) | [:ACTED_IN {roles: ["Emil"]}] | (:Person {name: "Emil Eifrem"}) |
| (:Movie) | [:ACTED_IN {roles: ["Agent Smith"]}] | (:Person {name: "Hugo Weaving}) |
| (:Movie) | [:ACTED_IN {roles: ["Morpheus"]}] | (:Person {name: "Laurence Fishburne"}) |
| (:Movie) | [:ACTED_IN {roles: ["Trinity"]}] | (:Person {name: "Carrie-Anne Moss"}) |
| (:Movie) | [:ACTED_IN {roles: ["Neo"]}] | (:Person {name: "Keanu Reeves"}) |
+------------------------------------------------------------------------------------------+ マッピングの結果は使用できない可能性が高くなります。これがリストにマッピングされる場合、Movie の重複が含まれることになりますが、このムービーには 1 つの関連しかありません。
ルートノードごとに 1 つのレコードを取得する
正しいオブジェクトを取得するには、クエリ内のリレーションシップと関連ノードを収集する必要があります: MATCH (m:Movie{title: 'The Matrix'})←[r:ACTED_IN]-(p:Person) return m,collect(r),collect(p)
+------------------------------------------------------------------------+ | m | collect(r) | collect(p) | +------------------------------------------------------------------------+ | (:Movie) | [[:ACTED_IN], [:ACTED_IN], ...]| [(:Person), (:Person),...] | +------------------------------------------------------------------------+
この結果を 1 つのレコードとして使用すると、Spring Data Neo4j はすべての関連ノードをルートノードに正しく追加できます。
グラフをより深く理解する
上の例では、関連ノードの最初のレベルのみをフェッチしようとしていると想定しています。これでは不十分な場合があり、マップされたインスタンスの一部である必要があるノードがグラフのさらに深いところに存在する可能性があります。これを実現するには、データベース側またはクライアント側の削減の 2 つの方法があります。
このため、上記の例には、最初の Movie とともに返される Persons 上の Movies も含まれている必要があります。

データベース側の削減
Spring Data Neo4j はレコードベースのみを適切に処理できることに留意し、1 つのエンティティインスタンスの結果は 1 つのレコードに含まれる必要があります。サイファーの道 (英語) 機能の使用は、グラフ内のすべての ブランチをフェッチする有効なオプションです。
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN p; これにより、複数のパスが 1 つのレコード内でマージされなくなります。collect(p) を呼び出すことは可能ですが、Spring Data Neo4j はマッピングプロセスにおけるパスの概念を理解していません。結果としてノードと関連を抽出する必要があります。
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN m, nodes(p), relationships(p);「マトリックス」から別のムービーに至る経路は複数あるため、結果は依然として 1 つの記録にはなりません。ここで Cypher の reduce 関数 (英語) が活躍します。
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
WITH collect(p) as paths, m
WITH m,
reduce(a=[], node in reduce(b=[], c in [aa in paths | nodes(aa)] | b + c) | case when node in a then a else a + node end) as nodes,
reduce(d=[], relationship in reduce(e=[], f in [dd in paths | relationships(dd)] | e + f) | case when relationship in d then d else d + relationship end) as relationships
RETURN m, relationships, nodes;reduce 関数を使用すると、さまざまなパスからのノードと関連を平坦化できます。結果として、ルートノードごとに 1 つのレコードを取得するに似たタプルが得られますが、コレクション内に関連型またはノードが混在しています。
クライアント側の削減
クライアント側で削減が行われる必要がある場合、Spring Data Neo4j を使用すると、リレーションシップまたはノードのリストのリストもマップできます。ただし、返されるレコードには、結果として得られるエンティティインスタンスを正しくハイドレートするためのすべての情報が含まれている必要があるという要件が適用されます。
MATCH p=(m:Movie{title: 'The Matrix'})<-[:ACTED_IN]-(:Person)-[:ACTED_IN*..0]->(:Movie)
RETURN m, collect(nodes(p)), collect(relationships(p)); 追加の collect ステートメントは、次の形式でリストを作成します。
[[rel1, rel2], [rel3, rel4]]
これらのリストは、マッピングプロセス中にフラットリストに変換されるようになります。
クライアント側とデータベース側のどちらの削減を選択するかは、生成されるデータの量によって決まります。reduce 関数を使用する場合は、最初にすべてのパスをデータベースのメモリ内に作成する必要があります。一方、クライアント側で大量のデータをマージする必要があると、そこでのメモリ使用量が増加します。 |
パスを使用してエンティティのリストを設定して返す
次のようなグラフが表示されます。

マッピングに示されているドメインモデル (簡潔にするためにコンストラクターとアクセサーは省略されています)。
@Node
public class SomeEntity {
@Id
private final Long number;
private String name;
@Relationship(type = "SOME_RELATION_TO", direction = Relationship.Direction.OUTGOING)
private Set<SomeRelation> someRelationsOut = new HashSet<>();
}
@RelationshipProperties
public class SomeRelation {
@RelationshipId
private Long id;
private String someData;
@TargetNode
private SomeEntity targetPerson;
}
ご覧のとおり、関連は発信的なものだけです。生成されたファインダーメソッド ( findById を含む) は常に、マップされるルートノードとの一致を試みます。それ以降、すべての関連オブジェクトがマッピングされます。1 つのオブジェクトのみを返す必要があるクエリでは、そのルートオブジェクトが返されます。多くのオブジェクトを返すクエリでは、一致するすべてのオブジェクトが返されます。返されたオブジェクトからの出力および受信関連には、当然のことながら値が設定されます。
次の Cypher クエリを想定します。
MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
RETURN leaf, collect(nodes(p)), collect(relationships(p))これはルートノードごとに 1 つのレコードを取得するの推奨に従っており、ここで一致させたいリーフノードに最適です。ただし、これは、0 または 1 個のマップされたオブジェクトを返すすべてのシナリオにのみ当てはまります。このクエリは以前と同様にすべてのリレーションシップを設定しますが、4 つのオブジェクトすべてを返すわけではありません。
これは、パス全体を返すことで変更できます。
MATCH p = (leaf:SomeEntity {number: $a})-[:SOME_RELATION_TO*]-(:SomeEntity)
RETURN p ここでは、パス p が実際に 4 つのノードすべてへのパスを含む 3 行を返すという事実を利用します。4 つのノードすべてが設定され、リンクされて返されます。
カスタムクエリのパラメーター
これは、Neo4j ブラウザーまたは Cypher-Shell で $ 構文を使用して発行される標準の Cypher クエリとまったく同じ方法で実行できます (Neo4j 4.0 以降では、Cypher パラメーターの古い {foo} 構文がデータベースから削除されています)。
public interface ARepository extends Neo4jRepository<AnAggregateRoot, String> {
@Query("MATCH (a:AnAggregateRoot {name: $name}) RETURN a") (1)
Optional<AnAggregateRoot> findByCustomQuery(String name);
}
| 1 | ここではパラメーターを名前で参照しています。代わりに $0 などを使用することもできます。 |
名前付きパラメーターを追加のアノテーションなしで機能させるには、Java 8+ プロジェクトを -parameters でコンパイルする必要があります。Spring Boot Maven および Gradle プラグインは、これを自動的に実行します。何らかの理由でこれが不可能な場合は、@Param を追加して名前を明示的に指定するか、パラメーターインデックスを使用できます。 |
カスタムクエリでアノテーションが付けられた関数にパラメーターとして渡されたマップされたエンティティ ( @Node を持つすべてのもの) は、ネストされたマップに変換されます。次の例は、構造体を Neo4j パラメーターとして表します。
ムービーモデルに示されているように、アノテーションが付けられた Movie、Vertex、Actor クラスが与えられています。
@Node
public final class Movie {
@Id
private final String title;
@Property("tagline")
private final String description;
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
private final List<Actor> actors;
@Relationship(value = "DIRECTED", direction = Direction.INCOMING)
private final List<Person> directors;
}
@Node
public final class Person {
@Id @GeneratedValue
private final Long id;
private final String name;
private Integer born;
@Relationship("REVIEWED")
private List<Movie> reviewed = new ArrayList<>();
}
@RelationshipProperties
public final class Actor {
@RelationshipId
private final Long id;
@TargetNode
private final Person person;
private final List<String> roles;
}
interface MovieRepository extends Neo4jRepository<Movie, String> {
@Query("MATCH (m:Movie {title: $movie.__id__})\n"
+ "MATCH (m) <- [r:DIRECTED|REVIEWED|ACTED_IN] - (p:Person)\n"
+ "return m, collect(r), collect(p)")
Movie findByMovie(@Param("movie") Movie movie);
}
Movie のインスタンスを上記のリポジトリメソッドに渡すと、次の Neo4j マップパラメーターが生成されます。
{
"movie": {
"__labels__": [
"Movie"
],
"__id__": "The Da Vinci Code",
"__properties__": {
"ACTED_IN": [
{
"__properties__": {
"roles": [
"Sophie Neveu"
]
},
"__target__": {
"__labels__": [
"Person"
],
"__id__": 402,
"__properties__": {
"name": "Audrey Tautou",
"born": 1976
}
}
},
{
"__properties__": {
"roles": [
"Sir Leight Teabing"
]
},
"__target__": {
"__labels__": [
"Person"
],
"__id__": 401,
"__properties__": {
"name": "Ian McKellen",
"born": 1939
}
}
},
{
"__properties__": {
"roles": [
"Dr. Robert Langdon"
]
},
"__target__": {
"__labels__": [
"Person"
],
"__id__": 360,
"__properties__": {
"name": "Tom Hanks",
"born": 1956
}
}
},
{
"__properties__": {
"roles": [
"Silas"
]
},
"__target__": {
"__labels__": [
"Person"
],
"__id__": 403,
"__properties__": {
"name": "Paul Bettany",
"born": 1971
}
}
}
],
"DIRECTED": [
{
"__labels__": [
"Person"
],
"__id__": 404,
"__properties__": {
"name": "Ron Howard",
"born": 1954
}
}
],
"tagline": "Break The Codes",
"released": 2006
}
}
} ノードはマップで表されます。マップには常に、マップされた ID プロパティである id が含まれます。labels では、静的ラベルと動的ラベルのすべてが使用可能になります。すべてのプロパティ (および関連の種類) は、エンティティが SDN によって書き込まれた場合にグラフに表示されるのと同じように、これらのマップに表示されます。値は正しい Cypher 型を持ち、それ以上の変換は必要ありません。
すべての関連はマップのリストです。動的関連はそれに応じて解決されます。1 対 1 の関連もシングルトンリストとして直列化されます。人々間の 1 対 1 のマッピングにアクセスするには、これを $person.__properties__.BEST_FRIEND[0].__target__.__id__ と書くことになります。 |
エンティティが、異なる型の他のノードと同じ型の関連を持っている場合、すべて同じリストに表示されます。このようなマッピングが必要で、カスタムパラメーターを操作する必要がある場合は、それに応じてマッピングを展開する必要があります。これを行う 1 つの方法は、相関サブクエリです (Neo4j 4.1+ が必要です)。
カスタムクエリの Spring 式言語
Spring 式言語 (SpEL) は、:#{} 内のカスタムクエリで使用できます。ここでのコロンはパラメーターを指しており、パラメーターが意味をなす場合にはそのような式を使用する必要があります。ただし、リテラル拡張機能を使用すると、標準の Cypher でパラメーターが許可されない場所 (ラベルや関連型など) で SpEL 式を使用できます。これは、SpEL 評価を受けるクエリ内のテキストブロックを定義する標準的な Spring Data 方法です。
次の例では、基本的に上記と同じクエリを定義しますが、WHERE 句を使用してさらなる 波括弧 を回避します。
public interface ARepository extends Neo4jRepository<AnAggregateRoot, String> {
@Query("MATCH (a:AnAggregateRoot) WHERE a.name = :#{#pt1 + #pt2} RETURN a")
Optional<AnAggregateRoot> findByCustomQueryWithSpEL(String pt1, String pt2);
}
ブロックされた SpEL は :#{ で始まり、指定された String パラメーターを名前 (#pt1) で参照します。これを上記の Cypher 構文と混同しないでください。SpEL 式は、両方のパラメーターを 1 つの値に連結し、最終的に Neo4jClient に渡します。SpEL ブロックは } で終わります。
SpEL は、さらに 2 つの問題も解決します。Sort オブジェクトをカスタムクエリに渡すことができる 2 つの拡張機能が提供されています。カスタムクエリのリスト 63 を覚えていますか ? orderBy 拡張機能を使用すると、動的ソートを備えた Pageable をカスタムクエリに渡すことができます。
public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
@Query(""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
+ ":#{orderBy(#pageable)} SKIP $skip LIMIT $limit" (1)
)
Slice<Person> findSliceByName(String name, Pageable pageable);
@Query(""
+ "MATCH (n:Person) WHERE n.name = $name RETURN n :#{orderBy(#sort)}" (2)
)
List<Person> findAllByName(String name, Sort sort);
}
| 1 | Pageable は、SpEL コンテキスト内では常に pageable という名前を持ちます。 |
| 2 | Sort は、SpEL コンテキスト内では常に sort という名前を持ちます。 |
Spring 式言語拡張機能
リテラル拡張
literal 拡張機能を使用すると、カスタムクエリでラベルや関連型などを「動的」にすることができます。Cypher ではラベルも関連型もパラメーター化できないため、リテラルを指定する必要があります。
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
@Query("MATCH (n:`:#{literal(#label)}`) RETURN n") (1)
List<Inheritance.BaseClass> findByLabel(String label);
}
| 1 | literal 拡張子は、評価されたパラメーターのリテラル値に置き換えられます。 |
ここでは、ラベル上で動的に一致させるために literal 値が使用されています。メソッドにパラメーターとして SomeLabel を渡すと、MATCH (n: が生成されます。値を正しくエスケープするために目盛りが追加されました。これはおそらくすべての場合に必要なことではないため、SDN がこれを行うことはありません。SomeLabel) RETURN n
リスト拡張子
複数の値の場合は、すべての値の & または | 連結リストをレンダリングする allOf および anyOf が用意されています。
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
@Query("MATCH (n:`:#{allOf(#label)}`) RETURN n")
List<Inheritance.BaseClass> findByLabels(List<String> labels);
@Query("MATCH (n:`:#{anyOf(#label)}`) RETURN n")
List<Inheritance.BaseClass> findByLabels(List<String> labels);
}
ラベルの参照
ノードをドメインオブジェクトにマップする方法はすでに知っています。
@Node(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"})
public class BikeNode {
@Id String id;
String name;
}
このノードにはいくつかのラベルがあり、カスタムクエリで常に繰り返すとエラーが発生しやすくなります。1 つを忘れたり、型ミスをしたりする可能性があります。これを軽減するために、#{#staticLabels} という式を提供します。これはコロンで始まっていないことに注意してください。@Query アノテーションが付けられたリポジトリメソッドで使用します。
#{#staticLabels} の動作 public interface BikeRepository extends Neo4jRepository<Bike, String> {
@Query("MATCH (n:#{#staticLabels}) WHERE n.id = $nameOrId OR n.name = $nameOrId RETURN n")
Optional<Bike> findByNameOrId(@Param("nameOrId") String nameOrId);
}
このクエリは次のように解決されます
MATCH (n:`Bike`:`Gravel`:`Easy Trail`) WHERE n.id = $nameOrId OR n.name = $nameOrId RETURN nnameOrId の標準パラメーターをどのように使用したかに注目してください。ほとんどの場合、ここで SpEL 式を追加して物事を複雑にする必要はありません。
空間型
Spring Data Neo4j は次の空間型をサポートします
サポートされている変換
Spring Data 共通の
Point(データベース内の WGS 84-2D/SRID 4326 ポイントである必要があります )GeographicPoint2d(WGS84 2D/SRID 4326)GeographicPoint3d(WGS84 3D/SRID 4979)CartesianPoint2d(デカルト 2D/SRID 7203)CartesianPoint3d(デカルト 3D/SRID 9157)
派生ファインダーキーワード
ネイティブ Neo4j Java ドライバー org.neo4j.driver.types.Point 型を使用している場合は、派生ファインダーメソッドで次のキーワードとパラメーター型を使用できます。
エリア内のクエリ:
findBy[…]Within(org.springframework.data.geo.Circle circle)findBy[…]Within(org.springframework.data.geo.Box box)findBy[…]Within(org.springframework.data.neo4j.repository.query.BoundingBox boundingBox)
org.springframework.data.geo.Polygon を使用することもできますが、BoundingBox#of を呼び出して BoundingBox に渡す必要があります。 |
特定の点付近でクエリを実行します。
findBy[…]Near(org.neo4j.driver.types.Point point)- 指定された点までの距離で昇順にソートされた結果を返しますfindBy[…]Near(Point point, org.springframework.data.geo.Distance max)findBy[…]Near(Point point, org.springframework.data.domain.Range<Distance> between)findBy[…]Near(Range<Distance> between, Point p)
ログ
Spring Data Neo4j は、バージョン 7.1.5 以降、暗号化通知 (英語) 用の複数のロガーを提供します。ロガー org.springframework.data.neo4j.cypher には、Spring Data Neo4j によって呼び出されたすべてのステートメントと、サーバーから送信されたすべての通知が含まれます。一部のカテゴリを除外または昇格するために、次のロガーが用意されています。
org.springframework.data.neo4j.cypher.performanceorg.springframework.data.neo4j.cypher.hintorg.springframework.data.neo4j.cypher.unrecognizedorg.springframework.data.neo4j.cypher.unsupportedorg.springframework.data.neo4j.cypher.deprecationorg.springframework.data.neo4j.cypher.genericorg.springframework.data.neo4j.cypher.securityorg.springframework.data.neo4j.cypher.topology
SDN+OGM から SDN への移行
過去の SDN+OGM 移行に関する既知の課題
SDN+OGM には長年にわたってかなりの歴史があり、大規模なアプリケーションシステムの移行は楽しいものでも、すぐに利益が得られるものでもないことを理解しています。古いバージョンの Spring Data Neo4j から新しいバージョンに移行するときに確認された主な課題は、おおよそ次の順序で発生します。
- 複数のメジャーアップグレードをスキップした場合
Neo4j-OGM はスタンドアロンで使用できますが、Spring Data Neo4j はスタンドアロンで使用できません。これは Spring Data の拡張に大きく依存し、Spring Framework 自体にも大きく依存し、最終的にはアプリケーションの大部分に影響を与えます。アプリケーションがどのように構造化されているか、つまりフレームワーク部分がビジネスコードにどの程度漏れているかに応じて、アプリケーションを適応させる必要がさらに大きくなります。アプリケーションに複数の Spring Data モジュールがあり、グラフデータベースと同じサービス層にあるリレーショナルデータベースにアクセスした場合、事態はさらに悪化します。2 つのオブジェクトマッピングフレームワークを更新するのは楽しいことではありません。
- Spring Data 自体を通じて構成された組み込みデータベースに依存する
SDN+OGM プロジェクトの組み込みデータベースは Neo4j-OGM によって構成されます。Neo4j 3.0 から 3.5 にアップグレードしたいとします。アプリケーション全体をアップグレードする必要があります。何故ですか? データベースをアプリケーションに埋め込むことを選択すると、この埋め込みデータベースを構成するモジュールに自分自身を結び付けることになります。別の組み込みデータベースバージョンを使用するには、古いデータベースは新しいデータベースをサポートしていないため、それを構成したモジュールをアップグレードする必要があります。Neo4j-OGM に対応する Spring Data バージョンが常に存在するため、それもアップグレードする必要があります。ただし、Spring Data は Spring Framework に依存するため、最初の箇条書きの引数が適用されます。
- どの構成要素を含めるべきかわからない
条件を正しく理解するのは簡単ではありません。SDN+OGM 設定の構成要素をここに書きました。それらはすべて偶然に追加され、多くの競合する依存関係に対処している可能性があります。
| これらの観測に基づいて、SDN+OGM から SDN に切り替える前に、現在のアプリケーションで Bolt または http トランスポートのみを使用していることを確認することをお勧めします。アプリケーションとアプリケーションのアクセス層は、データベースのバージョンから大幅に独立しています。その状態から、SDN+OGM から SDN への移行を検討してください。 |
SDN+OGM Lovelace または SDN+OGM Moore から SDN への移行の準備
| Lovelace リリーストレインは SDN 5.1.x および OGM 3.1.x に対応し、Moore は SDN 5.2.x および OGM 3.2.x に対応します。 |
まず、アプリケーションが、Bolt プロトコルを介してサーバーモードで Neo4j に対して実行されることを確認する必要があります。これは、次の 3 つのケースのうち 2 つで動作することを意味します。
埋め込まれています
org.neo4j:neo4j-ogm-embedded-driver と org.neo4j:neo4j をプロジェクトに追加し、OGM 機能を介してデータベースを開始しました。これはサポートされなくなったため、標準の Neo4j サーバーをセットアップする必要があります (スタンドアロンとクラスターの両方がサポートされています)。
上記の依存関係を削除する必要があります。
組み込みソリューションからの移行は、サーバーもセットアップする必要があるため、おそらく最も困難な移行です。ただし、それ自体に大きな価値があるものです。将来的には、アプリケーションフレームワークやデータアクセスフレームワークを考慮することなく、データベース自体をアップグレードできるようになります。
HTTP トランスポートを使用している
org.neo4j:neo4j-ogm-http-driver を追加し、http://user:password@localhost:7474 (英語) のような URL を構成しました。依存関係を org.neo4j:neo4j-ogm-bolt-driver に置き換える必要があり、bolt://localhost:7687 のようなボルト URL を構成するか、ルーティングも処理する新しい neo4j:// スキームを使用する必要があります。
マイグレーション
SDN+OGM アプリケーションが Bolt 上で期待どおりに動作することを確認したら、SDN への移行を開始できます。
すべての
org.neo4j:neo4j-ogm-*依存関係を削除しますorg.neo4j.ogm.config.ConfigurationBean を介した SDN の構成はサポートされていません。代わりに、ドライバーのすべての構成が新しい Java ドライバースターターを介して行われます。特に URL と認証のプロパティを調整する必要があります。リスト 112 を参照してください。
| XML を介して SDN を構成することはできません。SDN+OGM アプリケーションでこれを行った場合は、Spring アプリケーションのアノテーション駆動型または関数構成について必ず学習してください。最近では Spring Boot が最も簡単な選択です。スターターを配置すると、接続 URL と認証を除く必要な部分がすべてすでに構成されています。 |
# Old
spring.data.neo4j.embedded.enabled=false # No longer supported
spring.data.neo4j.uri=bolt://localhost:7687
spring.data.neo4j.username=neo4j
spring.data.neo4j.password=secret
# New
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret| これらの新しいプロパティは、将来、SDN とドライバーが古い設定を完全に置き換えたときに、再び変更される可能性があります。 |
最後に、新しい依存関係を追加します。Gradle と Maven の両方については、第 8 章を参照してください。
これで、アノテーションを置き換える準備が整いました。
| 古い | 新規 |
|---|---|
|
|
|
|
|
|
|
|
|
|
| 交換不要、不要 |
| 交換不要、不要 |
| 射影を使用します。任意の結果マッピングはサポートされなくなりました |
| いくつかの Neo4j-OGM アノテーションには、SDN に対応するアノテーションがまだ存在せず、今後も存在しないものもあります。追加の機能がサポートされると、上記のリストに追加されます。 |
ブックマーク管理
@EnableBookmarkManagement と @UseBookmark の両方、org.springframework.data.neo4j.bookmark.BookmarkManager インターフェースとその唯一の実装である org.springframework.data.neo4j.bookmark.CaffeineBookmarkManager は廃止され、必要なくなりました。
SDN は、構成を行わずに、すべてのトランザクションにブックマークを使用します。CaffeineBookmarkManager の Bean 宣言と com.github.ben-manes.caffeine:caffeine への依存関係を削除できます。
どうしても必要な場合は、次の手順に従って自動ブックマーク管理を無効にすることができます。
制約とインデックスの自動作成
SDN 5.3 以前では、Neo4j-OGM の「自動インデックスマネージャー」が提供されていました。
@Index、@CompositeIndex、@Required は交換せずに取り外しました。なぜ? たとえスキーマのないデータベースであっても、スキーマの作成はドメインモデリングの一部ではないと考えています。SDN モデルがスキーマであると主張する人もいるかもしれませんが、コマンドとクエリの分離 [Wikipedia] (英語) の方が望ましいと答えるよりも、むしろ別々の読み取りモデルと書き込みモデルを定義することを意味します。これらは、「退屈な」ことを書いたり、グラフ状の回答を読んだりするのに非常に便利です。
それとは別に、これらのアノテーションの一部はそれぞれの値が特定の Neo4j エディションまたはバージョンに関連付けられているため、メンテナンスが困難になります。
ただし、最も有力な議論は本番環境です。スキーマを生成するすべてのツールは開発中に確かに役に立ちますが、厳格なスキームを強制するデータベースの場合はさらに役立ちますが、本番環境ではあまり役に立たない傾向があります。アプリケーションを同時に実行していますか ? バージョン A は、新しいバージョン B によって作成されたインデックスをアサートしますか ?
これについては事前に制御する方がよいと考えており、リキグラフ (英語) や Neo4j の移行 [GitHub] (英語) などのツールに基づいて、制御されたデータベース移行を使用することをお勧めします。後者は、JHipster プロジェクト内の SDN で使用されていることが確認されています。どちらのプロジェクトにも、スキーマの現在のバージョンをデータベース内に保存し、更新前にスキーマが期待どおりであることを確認するという共通点があります。
以前の Neo4j-OGM アノテーションからの移行は @Index、@CompositeIndex、@Required に影響します。その例はリスト 113 に示されています。
@CompositeIndex(properties = {"tagline", "released"})
public class Movie {
@Id @GeneratedValue Long id;
@Index(unique = true)
private String title;
private String description;
private String tagline;
@Required
private Integer released;
}
そのアノテーションは、Cypher の次のスキームと同等です (Neo4j 4.2 時点)。
CREATE CONSTRAINT movies_unique_title ON (m:Movie) ASSERT m.title IS UNIQUE;
CREATE CONSTRAINT movies_released_exists ON (m:Movie) ASSERT EXISTS (m.released);
CREATE INDEX movies_tagline_released_idx FOR (m:Movie) ON (m.tagline, m.released);unique = true を使用せずに @Index を使用することは、CREATE INDEX movie_title_index FOR (m:Movie) ON (m.title) と同等です。一意のインデックスにはすでにインデックスが含まれていることに注意してください。
Spring Data Neo4j の構築
要件
JDK 17+ (OpenJDK (英語) または Oracle JDK (英語) のいずれかになります)
Maven 3.8.5 (Maven ラッパーを提供します。プロジェクトルートの
mvnwおよびmvnw.cmdをそれぞれ参照してください。ラッパーは適切な Maven バージョンを自動的にダウンロードします)Neo4j 5.+ データベース、いずれか
ローカルで実行
または Testcontainers (英語) および Docker (英語) を介して間接的に
ビルドの実行
次のセクションは代替案であり、増加する労力によって大まかに分類されています。
すべてのビルドにはプロジェクトのローカルコピーが必要です。
$ git clone [email protected] (英語) :spring-projects/spring-data-neo4j.git続行する前に、ローカルにインストールされている JDK のバージョンを確認してください。出力は次のようになります。
$ java -version
java version "18.0.1" 2022-04-19
Java(TM) SE Runtime Environment (build 18.0.1+10-24)
Java HotSpot(TM) 64-Bit Server VM (build 18.0.1+10-24, mixed mode, sharing)Docker 装着時
デフォルトのイメージを使用する
Docker [Wikipedia] (英語) がインストールされていない場合は、Docker デスクトップ (英語) に進んでください。つまり、Docker は、いわゆるコンテナーで OS レベルの仮想化を使用して軽量のソフトウェアイメージを実行するのに役立つツールです。
ビルドではテストコンテナー Neo4j (英語) を使用してデータベースインスタンスを起動します。
$ ./mvnw clean verifyWindows マシンでは、次を使用します。
$ mvnw.cmd clean verify出力は同様になるはずです。
別のイメージを使用する
使用するイメージのバージョンは、次のような環境変数を通じて構成できます。
$ SDN_NEO4J_VERSION=5.3.0-enterprise SDN_NEO4J_ACCEPT_COMMERCIAL_EDITION=yes ./mvnw clean verifyここでは 5.3.0 enterprise を使用し、ライセンス契約にも同意します。
環境変数をインラインで指定しても機能しない場合は、環境変数の定義方法についてオペレーティングシステムまたはシェルのマニュアルを参照してください。
ローカルで実行されているデータベースに対して
| ローカルで実行されているデータベースに対して実行すると、その内容全体が消去されます。 |
毎回コンテナーを再起動する必要がないため、ローカルで実行されているデータベースに対するビルドが高速になります。開発中にこれを頻繁に行います。
Neo4j のコピーは、ダウンロードセンター (英語) からフリーで入手できます。
ご使用のオペレーティングシステムに該当するバージョンをダウンロードし、指示に従って起動してください。必要な手順は、データベースを起動した後にブラウザーを開いて http://localhost:7474 に移動し、デフォルトのパスワードを neo4j から好みのパスワードに変更することです。
その後、ローカル bolt URL を指定して完全なビルドを実行できます。
$ SDN_NEO4J_URL=bolt://localhost:7687 SDN_NEO4J_PASSWORD=verysecret ./mvnw clean verifyビルドを制御する環境変数の概要
| 名前 | デフォルト値 | 意味 |
|---|---|---|
| 5.3.0 | 使用する Neo4j docker イメージのバージョン。Neo4j Docker 公式イメージ (英語) を参照 |
| いいえ | 一部のテストでは、Neo4j のエンタープライズエディションが必要になる場合があります。当社は社内でエンタープライズエディションを構築してテストしていますが、ライセンスの受け入れを希望しない場合は、強制的にライセンスを受け入れることはありません。 |
| 未設定 | この環境を設定すると、ローカルで実行されている Neo4j インスタンスに接続できるようになります。開発中にこれをよく使用します。 |
| 未設定 |
|
ローカルインスタンスを使用するには、SDN_NEO4J_URL と SDN_NEO4J_PASSWORD の両方を設定する必要があります。 |
付録 B: リポジトリクエリキーワード
サポートされているクエリメソッドの件名キーワード
次の表に、述語を表現するために Spring Data リポジトリのクエリ導出メカニズムで一般的にサポートされているサブジェクトキーワードを示します。ここにリストされている一部のキーワードは特定のストアでサポートされていない可能性があるため、サポートされているキーワードの正確なリストについては、ストア固有のドキュメントを参照してください。
| キーワード | 説明 |
|---|---|
| 通常、リポジトリ型、 |
| 射影が存在し、通常は |
| 数値結果を返す射影をカウントします。 |
| 結果なし( |
| クエリ結果を結果の最初の |
| 一意のクエリを使用して、一意の結果のみを返します。その機能がサポートされているかどうかは、ストア固有のドキュメントを参照してください。このキーワードは、 |
サポートされているクエリメソッドの述語キーワードと修飾子
次の表に、Spring Data リポジトリクエリ派生メカニズムで一般的にサポートされている述語キーワードを示します。ただし、ここにリストされている一部のキーワードは特定のストアでサポートされていない可能性があるため、サポートされているキーワードの正確なリストについては、ストア固有のドキュメントを参照してください。
| 論理キーワード | キーワード表現 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
フィルター述語に加えて、次の修飾子のリストがサポートされています。
| キーワード | 説明 |
|---|---|
| 大文字と小文字を区別しない比較のために、述語キーワードとともに使用されます。 |
| すべての適切なプロパティの大文字と小文字を区別しません。クエリメソッド述語のどこかで使用されます。 |
| 静的な並べ替え順序を指定し、その後にプロパティのパスと方向を指定します(例: |
付録 C: リポジトリクエリの戻り値の型
サポートされているクエリの戻り値の型
次の表に、Spring Data リポジトリで一般的にサポートされる戻り値の型を示します。ただし、ここにリストされている一部の型は特定のストアでサポートされていない可能性があるため、サポートされる戻り値の型の正確なリストについてはストア固有のドキュメントを参照してください。
地理空間型(GeoResult、GeoResults、GeoPage など)は、地理空間クエリをサポートするデータストアでのみ使用できます。一部のストアモジュールは、独自の結果ラッパー型を定義する場合があります。 |
| 戻りの型 | 説明 |
|---|---|
| 戻り値がないことを示します。 |
プリミティブ | Java プリミティブ。 |
ラッパーの種類 | Java ラッパー型。 |
| 一意のエンティティ。クエリメソッドが最大で 1 つの結果を返すことを期待します。結果が見つからない場合、 |
|
|
|
|
|
|
| Java 8 または Guava |
| Scala または Vavr |
| Java 8 |
|
|
|
|
Vavr | Vavr コレクション型。詳細については、セクション 10.4.6.3 を参照してください。 |
|
|
| Java 8 |
| 使用可能なデータがさらにあるかどうかを示すサイズのデータチャンク。 |
| 結果の総数などの追加情報を含む |
| 参照場所までの距離などの追加情報を含む結果エントリ。 |
| 参照場所までの平均距離などの追加情報を含む |
| 参照位置までの平均距離など、 |
| リアクティブリポジトリを使用して 0 個または 1 個の要素を放出する Project Reactor |
| リアクティブリポジトリを使用してゼロ、1 つ、多くの要素を放出する Project Reactor |
| リアクティブリポジトリを使用して単一の要素を放出する RxJava |
| リアクティブリポジトリを使用して 0 個または 1 個の要素を放出する RxJava |
| リアクティブリポジトリを使用してゼロ、1 つ、多くの要素を放出する RxJava |