© 2008-2021 The original authors.
| このドキュメントのコピーは、あなた自身の使用および他者への配布のために作成することができますが、そのようなコピーに料金を請求しないこと、さらに、印刷物または電子的に配布されるかどうかにかかわらず、各コピーにこの著作権表示が含まれていることを条件とします。 |
序文
1. このドキュメントの手順
Spring Data の核となる概念をすでに理解している場合は、そのまま第 5 章に進んでください。この章では、アプリケーションを Neo4j インスタンスに接続するように構成するためのさまざまなオプションと、ドメインをモデル化する方法について説明します。
ほとんどの場合、ドメインが必要になります。ノードと関連をドメインモデルにマップする方法については、第 6 章に移動してください。
その後、ドメインをクエリする何らかの手段が必要になります。選択肢は、Neo4j リポジトリ、Neo4j テンプレート、または下位レベルの Neo4j クライアントです。これらはすべて、リアクティブ方式でも利用できます。ページングメカニズムとは別に、標準リポジトリのすべての機能がリアクティブバリアントで利用できます。
次の章では、その構成要素について説明します。
リポジトリの一般的な概念について詳しくは、第 7 章を参照してください。
もちろん、序文と優しい入門ガイドを読み続けてください。
2. NoSQL およびグラフデータベース
グラフデータベースは、膨大な情報ネットワークの保存と取得に特化したストレージエンジンです。データを他のノードまたは同じノードとの関連を持つノードとして効率的に保存するため、それらの構造の高パフォーマンスな検索とクエリが可能になります。プロパティはノードと関連の両方に追加できます。ノードには 0 個以上のラベルを付けることができ、関連は常に指示され、名前が付けられます。
グラフデータベースは、ほとんどの種類のドメインモデルの保存に適しています。ほとんどすべての領域で、特定のものが他のものと接続されています。他のほとんどのモデリング手法では、物事間の関連は、アイデンティティや属性のない単一のリンクに還元されます。グラフデータベースを使用すると、関連を「物」としてモデル化することなく、ドメインに由来する豊富な関連をデータベース内で同様に適切に表現した状態に保つことができます。実際のドメインをグラフデータベースに入れる場合、「インピーダンスの不一致」はほとんどありません。
2.1. Neo4j の導入
Neo4j (英語) は、オープンソースの NoSQL グラフデータベースです。これは、関連によって接続されたノードで構成されるグラフとして構造化されたデータを保存する、完全なトランザクションデータベース (ACID) です。現実世界の構造からインスピレーションを得たこのツールは、開発者にとって直感的でシンプルなままでありながら、複雑なデータに対する高いクエリパフォーマンスを可能にします。
Neo4j について学ぶための出発点は neo4j.com (英語) です。役立つリソースのリストは次のとおりです。
Neo4j ドキュメント (英語) には Neo4j が導入されており、スタートガイド、リファレンスドキュメント、チュートリアルへのリンクが含まれています。
オンラインサンドボックス (英語) は、オンラインチュートリアル (英語) と組み合わせて Neo4j インスタンスを操作する便利な方法を提供します。
ネオ 4j Java ボルトドライバー (英語)
数冊の書籍 (英語) を購入したり、ビデオ [YouTube] (英語) を視聴したりできます。
2.2. Spring および Spring Data
Spring Data は、IoC コンテナー、型変換システム、式言語、JMX 統合、ポータブル DAO 例外階層などの Spring Framework のコア機能を使用します。Spring API をすべて理解する必要はありませんが、その背景となる概念を理解することは必要です。少なくとも、IoC の背景となる考え方はよく知っている必要があります。
Spring Data Neo4j プロジェクトは、Neo4j グラフデータストアを使用したソリューションの開発に Spring Data の概念を適用します。当社は、ドキュメントを保存およびクエリするための高レベルの抽象化としてリポジトリを提供するだけでなく、汎用ドメインアクセスまたは汎用クエリ実行用のテンプレートとクライアントも提供します。それらはすべて、Spring のアプリケーショントランザクションと統合されています。
Neo4j サポートのコア機能は、Neo4jClient または Neo4jTemplate、あるいはそれらのリアクティブバリアントを通じて直接使用できます。これらはすべて、Spring のアプリケーションレベルのトランザクションとの統合を提供します。下位レベルでは、Bolt ドライバーインスタンスを取得できますが、独自のトランザクションを管理する必要があります。
Spring について詳しくは、Spring Framework について詳しく説明した包括的なドキュメントを参照してください。この問題に関する記事、ブログエントリ、書籍が多数あります。詳細については、Spring Framework ホームページを参照してください。
2.3. Spring Data Neo4j とは
現行の Spring Data Neo4j は Spring Data Neo4j+Neo4j-OGM の後継品です。Neo4j-OGM (Neo4j Object Graph Mapper) の別個のレイヤーは Spring インフラストラクチャに置き換えられましたが、Object Graph Mapper (OGM) の基本概念は引き続き適用されます。
OGM は、グラフ内のノードと関連をドメインモデル内のオブジェクトと参照にマップします。オブジェクトインスタンスはノードにマップされ、オブジェクト参照はリレーションシップを使用してマップされるか、プロパティ (日付への参照など) に直列化されます。JVM プリミティブは、ノードまたは関連のプロパティにマップされます。OGM はデータベースを抽象化し、低レベルのドライバーを直接使用することなく、ドメインモデルをグラフに保持し、クエリを実行するための便利なメソッドを提供します。また、SDN によって生成されたクエリでは不十分な場合に、開発者がカスタムクエリを提供できる柔軟性も提供されます。
2.3.1. 箱の中は何ですか?
Spring Data Neo4j、つまり SDN は、Neo4j 株式会社 (英語) によって作成および保守される次世代の Spring Data モジュールです。VMware Spring Data チームと緊密に連携しています。
SDN は、マッピングフレームワークとドライバーの間に別の「ドライバー」層や「トランスポート」層を導入することなく、Neo4j Java ドライバー [GitHub] (英語) に完全に依存しています。Neo4j Java ドライバー (ボルトまたはボルトドライバーとも呼ばれます) は、JDBC がリレーショナルデータベースで使用されるのとよく似たプロトコルとして使用されます。
新しい SDN を Spring Data Neo4j + OGM と区別する注目すべき機能は次のとおりです
不変エンティティの完全サポート、Kotlin のデータクラスの完全サポート
Spring Framework 自体および Spring Data でのリアクティブプログラミングモデルの完全なサポート
新しい Neo4j クライアントとリアクティブクライアント機能により、プレーンドライバーではなくテンプレートの概念が復活し、データベースアクセスが容易になります。
2.3.2. SDN+OGM ではなく SDN を使用する必要がある理由
SDN には、SDN+OGM にはないいくつかの機能があります。
リアクティブトランザクションを含む Springs リアクティブストーリーの完全サポート
例示による問い合わせの完全サポート
完全に不変のエンティティを完全にサポート
空間クエリを含む派生ファインダーメソッドのすべての修飾子とバリエーションのサポート
2.3.3. SDN は Neo4j-OGM とどのように関係しますか ?
Neo4j-OGM (英語) はオブジェクトグラフマッピングライブラリであり、主に Spring Data Neo4j の以前のバージョンで、ノードと関連をドメインオブジェクトにマッピングするという重労働を行うバックエンドとして使用されます。現在の SDN は Neo4j-OGM を必要とせず、サポートしていません。SDN は、クラスのスキャンとメタモデルの構築にのみ Spring Data のマッピングコンテキストを使用します。
これにより、SDN が Spring エコシステムに固定されますが、CPU とメモリの使用量、特に Spring のマッピングコンテキストのすべての機能に関するフットプリントが小さいなど、いくつかの利点があります。
2.3.5. SDN は組み込み Neo4j をサポートしていますか ?
組み込み Neo4j には複数の側面があります。
SDN は埋め込みインスタンスと直接対話しますか ?
いいえ。組み込みデータベースは通常、org.neo4j.graphdb.GraphDatabaseService のインスタンスによって表され、すぐに使用できるボルトコネクターはありません。
ただし、SDN は Neo4j のテストハーネスと非常によく連携します。テストハーネスは、実際のデータベースの代替として特別に設計されています。Neo4j 3.5 および 4.0 テストハーネスの両方のサポートは、ドライバーの Spring Boot スターター [GitHub] (英語) を介して実装されています。対応するモジュール org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure を参照してください。
3. ビルドブロック
3.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+ データベースが必要です。

3.2. パッケージレベル
| パッケージ | 説明 |
|---|---|
| このパッケージには、アプリケーション固有のアノテーション付き構成クラスに使用できる構成関連のサポートクラスが含まれています。Spring Boot の自動構成に依存しない場合は、抽象基本クラスが役立ちます。このパッケージには、監査を可能にする追加のアノテーションがいくつか用意されています。 |
| このパッケージには、クエリを実行できる命令型またはリアクティブクライアントを作成するためのコアインフラストラクチャが含まれています。 |
| SDN がサポートする一連のシンプルな型を提供します。 |
| このパッケージは、ドメインで役立つ可能性のあるいくつかのサポートクラスを提供します。たとえば、一部のトランザクションが再試行される可能性があることを示す述語や、追加のコンバーターや ID ジェネレーターなどです。 |
| アンマネージド Neo4j トランザクションを Spring マネージドトランザクションに変換するためのコアインフラストラクチャが含まれています。命令型とリアクティブの両方の |
| このパッケージは、Neo4j 命令型およびリアクティブリポジトリ API を提供します。 |
| Neo4j 固有のリポジトリの構成インフラストラクチャ、特に命令型およびリアクティブな Spring Data Neo4j リポジトリを有効にする専用のアノテーション。 |
| このパッケージは、カスタムの命令型およびリアクティブな Spring Data Neo4j リポジトリ基本クラスを構築するためのいくつかのパブリックサポートクラスを提供します。サポートクラスは、SDN 自体で使用されるクラスと同じです。 |
4. 依存関係
個々の Spring Data モジュールの開始日が異なるため、それらのほとんどは異なるメジャーバージョン番号とマイナーバージョン番号を持っています。互換性のあるものを見つける最も簡単な方法は、互換性のあるバージョンが定義された状態で提供される Spring Data リリーストレイン BOM に依存することです。Maven プロジェクトでは、次のように POM の <dependencyManagement /> セクションでこの依存関係を宣言します。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2021.0.2</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement> 現在のリリーストレインバージョンは 2021.0.2 です。トレインバージョンでは、パターン 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>4.1. Spring Boot を使用した依存関係管理
Spring Boot は、Spring Data モジュールの最新バージョンを選択します。それでも新しいバージョンにアップグレードする場合は、spring-data-releasetrain.version プロパティを使用するトレインバージョンとイテレーションに設定します。
リファレンスドキュメント
誰がこれを読むべきでしょうか ?
このマニュアルは以下を対象として書かれています。
Neo4j の Spring 統合を調査しているエンタープライズアーキテクト。
Neo4j を使用して Spring Data ベースのアプリケーションを開発するエンジニア。
5. 入門
SDN 用の Spring Boot スターターを提供しています。依存関係管理を使用してスターターモジュールを含め、使用する bolt URL (例: org.neo4j.driver.uri=bolt://localhost:7687) を設定してください。このスターターは、サーバーで認証が無効になっていることを前提としています。SDN スターターは Java ドライバー用のスターターに依存しているため、Java ドライバー用スターターで説明されている設定に関するすべての事項が、こちらにも適用されます。使用可能なプロパティについては、IDE の org.neo4j.driver 名前空間の自動補完機能を使用するか、専用のマニュアル [GitHub] (英語) を参照してください。
SDN のサポート
よく知られ理解されている命令型プログラミングモデル (Spring Data JDBC や JPA によく似ています)
リアクティブトランザクション (英語) の完全サポートを含む、Reactive Streams (英語) に基づくリアクティブプログラミング。
これらはすべて同じバイナリに含まれています。リアクティブプログラミングモデルでは、データベース側に 4.0 Neo4j サーバーが必要で、他方ではリアクティブ Spring が必要です。
5.1. データベースを準備する
この例では、すべての Neo4j インスタンスにフリーで付属しているため、ムービーグラフ (英語) 内に留まります。
実行中のデータベースはないが、Docker がインストールされている場合は、次を実行してください。
docker run --publish=7474:7474 --publish=7687:7687 -e 'NEO4J_AUTH=neo4j/secret' neo4j:4.2.4http://localhost:7474 にアクセスできることがわかります。上記のコマンドは、サーバーのパスワードを secret に設定します。プロンプトで実行できるコマンド (:play movies) に注意してください。これを実行して、データベースにテストデータを入力します。
5.2. 新しい Spring Boot プロジェクトを作成する
Spring Boot プロジェクトをセットアップする最も簡単な方法は、start.spring.io を使用することです (Web サイトを使用しない場合は、主要な IDE にも統合されています)。
"Spring Web Starter" を選択して、Spring ベースの Web アプリケーションの作成に必要なすべての依存関係を取得します。Spring Initializr は、選択したビルドツールのすべてのファイルと設定を使用して、有効なプロジェクト構造を作成します。
5.2.1. Maven の使用
Spring イニシャライザーに対して curl リクエストを発行して、基本的な Maven プロジェクトを作成できます。
curl https://start.spring.io/starter.tgz \
-d dependencies=webflux,actuator,data-neo4j \
-d bootVersion=2.4.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>既存のプロジェクトの場合は、依存関係を手動で追加することもできます。
5.2.2. Gradle の使用
考え方は同じで、Gradle プロジェクトを生成するだけです。
curl https://start.spring.io/starter.tgz \
-d dependencies=webflux,actuator,data-neo4j \
-d type=gradle-project \
-d bootVersion=2.4.1 \
-d baseDir=Neo4jSpringBootExampleGradle \
-d name=Neo4j%20SpringBoot%20Example | tar -xzvf -Gradle の依存関係は次のようになり、build.gradle に追加する必要があります。
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
}既存のプロジェクトの場合は、依存関係を手動で追加することもできます。
5.3. プロジェクトを構成する
次に、これらのプロジェクトのいずれかをお気に入りの IDE で開きます。application.properties を見つけて、Neo4j 認証情報を構成します。
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secretこれは、Neo4j インスタンスに接続するために必要な最低限のものです。
| このスターターを使用する場合、ドライバーのプログラム構成を追加する必要はありません。SDN リポジトリは、このスターターによって自動的に有効になります。 |
5.4. モジュールパス上で実行 (Java 9+)
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 からアクセスできるようにエクスポートする必要があります。世界にエクスポートしたくない場合は、それらのモジュールに制限できます。 |
5.5. ドメインを作成する
ドメイン層は次の 2 つのことを達成する必要があります。
グラフをオブジェクトにマッピングする
それらへのアクセスを提供する
5.5.1. ノードエンティティの例
SDN は、Kotlin の Java クラスと data クラスの両方で、変更不可能なエンティティを完全にサポートします。ここでは不変エンティティに焦点を当てます。リスト 7 はそのようなエンティティを示します。
| SDN は、Neo4j Java ドライバーがサポートするすべてのデータ型をサポートします。「Cypher 型システム」の章内の Neo4j 型をネイティブ言語型にマッピングする (英語) を参照してください。将来のバージョンでは追加のコンバーターがサポートされる予定です。 |
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.schema.Relationship.Direction;
@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 とともに使用する同じエンティティを次に示します。
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.annotation.PersistenceConstructor;
@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 内に留まりたい、またはその必要がある場合の代替手段です。
5.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 の一部になってしまいます。ほとんどの場合、後から何かを追加するよりも、何かを取り除く方が困難です。さらに、ストアの詳細を使用すると、ストアがドメインに漏洩します。パフォーマンスの観点からは、ペナルティはありません。
上記のムービーエンティティのいずれかに適合するリアクティブリポジトリは次のようになります。
import reactor.core.publisher.Mono;
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
public interface MovieRepository extends ReactiveNeo4jRepository<MovieEntity, String> {
Mono<MovieEntity> findOneByTitle(String title);
} リアクティブコードのテストは reactor.test.StepVerifier を使用して行われます。プロジェクト Reactor の対応するドキュメント (英語) を参照するか、サンプルコードを参照してください。 |
6. オブジェクトマッピング
次のセクションでは、グラフとドメイン間のマッピングのプロセスについて説明します。このプロセスは 2 つの部分に分かれています。最初の部分では、実際のマッピングと、ノード、関連、プロパティをオブジェクトにマッピングする方法を説明するために使用できるツールについて説明します。2 番目の部分では、Spring Data のオブジェクトマッピングの基礎について説明します。一般的なマッピング、不変のドメインオブジェクトを優先する理由、Java または Kotlin でモデル化する方法など、貴重なヒントが提供されます。
6.1. メタデータベースのマッピング
SDN 内のオブジェクトマッピング機能を最大限に活用するには、マップされたオブジェクトに @Node アノテーションを付ける必要があります。マッピングフレームワークにこのアノテーションがある必要はありませんが (アノテーションがなくても、POJO は正しくマップされます)、クラスパススキャナーがドメインオブジェクトを検索して前処理し、必要なメタデータを抽出できるようになります。このアノテーションを使用しない場合、ドメインオブジェクトを初めて保存するときにアプリケーションのパフォーマンスがわずかに低下します。これは、マッピングフレームワークがドメインオブジェクトのプロパティとその保存メソッドを認識できるように内部メタデータモデルを構築する必要があるためです。持続させます。
6.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: フィールドレベルで適用され、ノードの最終変更日を示します。@PersistenceConstructor: 1 つのコンストラクターに適用され、エンティティを読み取るときにそのコンストラクターを優先コンストラクターとしてマークします。@Persistent: クラスレベルで適用され、このクラスがデータベースへのマッピングの候補であることを示します。@Version: フィールドレベルで適用され、楽観的ロックに使用され、保存操作時に変更がチェックされます。@ReadOnlyProperty: フィールドレベルで適用され、プロパティを読み取り専用としてマークします。プロパティはデータベースの読み取り中にハイドレートされますが、書き込みの対象にはなりません。リレーションシップで使用する場合は、関連性がなければ、そのコレクション内の関連エンティティは永続化されないことに注意してください。
初期値はゼロで、更新のたびに自動的に増加します。
監査サポートに関するすべてのアノテーションについては、第 10 章を参照してください。
6.1.2. 基本的な構成要素: @Node
@Node アノテーションは、マッピングコンテキストによるクラスパススキャンの対象となる、クラスをマネージドドメインクラスとしてマークするために使用されます。
オブジェクトをグラフ内のノードにマッピングしたり、その逆を行うには、マッピング先またはマッピング元のクラスを識別するラベルが必要です。
@Node には属性 labels があり、アノテーション付きクラスのインスタンスの読み取りと書き込み時に使用する 1 つ以上のラベルを設定できます。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 が管理対象エンティティに存在する場合、結果として得られるラベルのセットはデータベースに書き込まれる「真実」になります。 |
6.1.3. インスタンスの識別: @Id
@Node はクラスと特定のラベルを持つノード間のマッピングを作成しますが、そのクラスの個々のインスタンス (オブジェクト) とノードのインスタンス間の接続も作成する必要があります。
ここで @Id が活躍します。@Id は、クラスの属性をオブジェクトの一意の識別子としてマークします。その一意の識別子は、最適な世界では一意のビジネスキー、言い換えれば自然キーです。@Id は、サポートされている単純型のすべての属性で使用できます。
ただし、自然キーを見つけるのは非常に困難です。たとえば、人の名前は一意であることはほとんどなく、時間の経過とともに変化したり、さらに悪いことに、誰もが姓名を持っているわけではありません。
2 つの異なる種類の代理キーをサポートします。
型 long または Long の属性では、@Id を @GeneratedValue とともに使用できます。これにより、Neo4j 内部 ID (ノードまたはリレーションシップのプロパティではなく、通常は表示されません) が属性にマップされ、SDN がクラスの個々のインスタンスを取得できるようになります。
@GeneratedValue は属性 generatorClass を提供します。generatorClass を使用して、IdGenerator を実装するクラスを指定できます。IdGenerator は関数インターフェースであり、その generateId はプライマリラベルとインスタンスを取得して ID を生成します。すぐに使える 1 つの実装として UUIDStringGenerator をサポートします。
generatorRef を介して @GeneratedValue のアプリケーションコンテキストから Spring Bean を指定することもできます。Bean も IdGenerator を実装する必要がありますが、データベースと対話するための Neo4j クライアントやテンプレートなど、コンテキスト内のすべてを利用できます。
| セクション 6.2 での ID の処理に関する重要な注意事項を無視しないでください |
6.1.4. マッピングプロパティ: @Property
@Node アノテーション付きクラスのすべての属性は、Neo4j ノードおよびリレーションシップのプロパティとして保持されます。さらに構成を行わなければ、Java または Kotlin クラスの属性の名前が Neo4j プロパティとして使用されます。
既存の Neo4j スキーマを使用している場合、またはマッピングをニーズに合わせて調整したい場合は、@Property を使用する必要があります。name は、データベース内のプロパティの名前を指定するために使用されます。
6.1.5. ノードの接続: @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 {
@Id @GeneratedValue
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 のプロパティを定義して、SDN が保存時にプロパティを失うことなく安全に上書きできる関連を判断できるようにする必要があります。SDN が内部ノード ID を格納するフィールドを見つけられない場合、起動時に失敗します。
@RelationshipProperties でアノテーションが付けられたクラスでサポートされている唯一の生成 ID フィールドは、上記のようにデフォルトの ID ジェネレーター InternalIdGenerator を使用した @GeneratedValue です。他のジェネレーターを使用すると、起動時にエラーが発生します。 |
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) (1)
private List<Roles> actorsAndRoles;関連クエリの備考
一般に、クエリを作成するための関連 / ホップに制限はありません。SDN は、モデル化されたノードから到達可能なグラフ全体を解析します。
つまり、関連を双方向にマッピングする、つまりエンティティの両端で関連を定義するという考えがある場合、期待以上の結果が得られる可能性があります。
ムービーに俳優がいて、すべての俳優を含む特定のムービーを取得したい例を考えてみましょう。ムービーから俳優への関連が一方向的なものであれば、これは問題になりません。双方向シナリオでは、SDN は特定の movie、その俳優だけでなく、関連の定義ごとにこの俳優に対して定義された他のムービーも取得します。最悪の場合、これは単一エンティティのグラフ全体のフェッチにまでカスケードされます。
6.1.6. 完全な例
これらすべてを組み合わせると、単純なドメインを作成できます。さまざまなロールを持つムービーや人物を使用します。
MovieEntityimport java.util.ArrayList;
import java.util.List;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.schema.Relationship.Direction;
@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 つのロールにマッピングされています。ドメインクラスは同じです。
PersonEntityimport org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
@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 を集約ルートとして、関連を所有していると考えています。一方で、関連するムービーをすべて選択せずに、データベースからすべての人物を取得できるようにしたいと考えています。データベース内のすべての関連をあらゆる方向にマッピングする前に、アプリケーションのユースケースを検討してください。これを行うことはできますが、オブジェクトグラフ内でグラフデータベースを再構築することになりかねません。これはマッピングフレームワークの目的ではありません。 |
6.2. 一意の ID の処理とプロビジョニング
6.2.1. Neo4j 内部 ID の使用
ドメインクラスに一意の識別子を付与する最も簡単なメソッドは、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 に関連付けられていますが、これはデータベースの存続期間全体にわたってのみアプリケーションエンティティに固有のものではありません。
不利益: 不変エンティティを作成するにはより多くの労力が必要です
6.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 です。
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.data.neo4j.core.schema.IdGenerator;
import org.springframework.util.StringUtils;
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;
}6.2.3. ビジネスキーの使用
完全な例の MovieEntity および PersonEntity ではビジネスキーを使用しています。個人の名前は、アプリケーションによって、および Spring Data を介してロードされる際の両方で、構築時に割り当てられます。
これは安定した一意のビジネスキーを見つけた場合にのみ可能ですが、優れた不変ドメインオブジェクトが作成されます。
利点: ビジネスキーまたは自然キーを主キーとして使用するのは自然なことです。問題のエンティティは明確に識別され、ほとんどの場合、ドメインのさらなるモデリングにおいて適切であると感じられます。
短所: 見つかったキーが思ったほど安定していないことがわかると、主キーとしてのビジネスキーを更新するのは難しくなります。たとえそうでないと約束したとしても、変更される可能性があることが判明することはよくあります。それとは別に、あるものにとって本当に一意な識別子を見つけるのは困難です。
6.3. Spring Data オブジェクトマッピングの基礎
このセクションでは、Spring Data オブジェクトのマッピング、オブジェクトの作成、フィールドとプロパティのアクセス、可変性と不変性の基本について説明します。
Spring Data オブジェクトマッピングの中心的なロールは、ドメインオブジェクトのインスタンスを作成し、ストアネイティブデータ構造をそれらにマッピングすることです。つまり、2 つの基本的な手順が必要です。
公開されたコンストラクターの 1 つを使用したインスタンスの作成。
すべての公開されたプロパティを具体化するインスタンスの設定。
6.3.1. オブジェクト作成
Spring Data は、その型のオブジェクトの具体化に使用される永続エンティティのコンストラクターを自動的に検出しようとします。解決アルゴリズムは次のように機能します。
引数のないコンストラクターがある場合は、それが使用されます。他のコンストラクターは無視されます。
引数を取る単一のコンストラクターがある場合は、それが使用されます。
引数を取る複数のコンストラクターがある場合、Spring Data が使用するコンストラクターに
@PersistenceConstructorのアノテーションを付ける必要があります。
値の解決は、コンストラクターの引数名がエンティティのプロパティ名と一致することを前提としています。つまり、マッピングのすべてのカスタマイズ(異なるデータストア列またはフィールド名など)を含む、プロパティが設定されるかのように解決が実行されます。また、これには、クラスファイルで使用可能なパラメーター名情報、またはコンストラクターに存在する @ConstructorProperties アノテーションのいずれかが必要です。
6.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(…) メソッドを公開します。インスタンスがデータストアに挿入され、識別子が生成されたとき。元の Person インスタンスは、新しいインスタンスが作成されるときに変更されません。通常、ストア管理される他のプロパティにも同じパターンが適用されますが、永続化操作のために変更する必要がある場合があります。 |
| 2 | firstname および lastname プロパティは、getter を介して潜在的に公開される通常の不変のプロパティです。 |
| 3 | age プロパティは不変ですが、birthday プロパティから派生しています。示されている設計では、Spring Data は宣言された唯一のコンストラクターを使用するため、データベース値はデフォルト設定よりも優先されます。計算が優先されることを意図している場合でも、このコンストラクターがパラメーターとして age をとることが重要です(それを無視する可能性がある)。そうしないと、プロパティ生成ステップは年齢フィールドを設定しようとし、不変であり、枯れています。 |
| 4 | comment プロパティは可変であり、フィールドを直接設定することで入力されます。 |
| 5 | remarks プロパティは可変であり、comment フィールドを直接設定するか、setter メソッドを呼び出して設定します。 |
| 6 | このクラスは、オブジェクト作成用のファクトリメソッドとコンストラクターを公開します。ここでの核となる考え方は、追加のコンストラクターの代わりにファクトリメソッドを使用して、@PersistenceConstructor によるコンストラクターの明確化の必要性を回避することです。代わりに、プロパティのデフォルト設定はファクトリメソッド内で処理されます。 |
6.3.3. 一般的な推奨事項
不変オブジェクトに固執するようにしてください — 不変オブジェクトは、オブジェクトを実体化するためにコンストラクターを呼び出すだけなので、簡単に作成できます。また、これにより、クライアントコードによるオブジェクトの状態の操作を可能にする setter メソッドがドメインオブジェクトに散在するのを防ぎます。これらが必要な場合は、同じ場所にある限られた種類の型によってのみ呼び出せるように、パッケージで保護することをお勧めします。コンストラクターのみの具体化は、プロパティの作成より最大 30% 高速です。
all-args コンストラクターを提供する — エンティティを不変の値としてモデル化できない、またはしたくない場合でも、オブジェクトのマッピングがプロパティの設定をスキップできるため、エンティティのすべてのプロパティを引数として取るコンストラクターを提供することには価値があります。最適なパフォーマンスのため。
@PersistenceConstructorを回避するために、オーバーロードされたコンストラクターの代わりにファクトリメソッドを使用します — 最適なパフォーマンスに必要なすべての引数コンストラクターでは、通常、自動生成識別子などを省略したアプリケーションユースケース固有のコンストラクターを公開します。これらの all-args コンストラクターのバリアントを公開する静的ファクトリメソッド。生成されたインスタンシエーターおよびプロパティアクセサークラスの使用を許可する制約を必ず遵守してください。
識別子を生成するには、final フィールドを wither メソッドと組み合わせて使用します。
Lombok を使用してボイラープレートコードを回避します — 永続化操作は通常、すべての引数を取るコンストラクターを必要とするため、その宣言はフィールド割り当てに対するボイラープレートパラメーターの退屈な繰り返しとなりますが、Lombok の
@AllArgsConstructorを使用することで回避することができます。
6.3.4. Kotlin サポート
Spring Data は、Kotlin の仕様を適合させて、オブジェクトの作成と変更を可能にします。
Kotlin オブジェクトの作成
Kotlin クラスはインスタンス化がサポートされており、すべてのクラスはデフォルトで不変であり、可変プロパティを定義するには明示的なプロパティ宣言が必要です。次の data クラス Person を検討してください。
data class Person(val id: String, val name: String) 上記のクラスは、明示的なコンストラクターを持つ典型的なクラスにコンパイルされます。別のコンストラクターを追加してこのクラスをカスタマイズし、@PersistenceConstructor でアノテーションを付けてコンストラクターの設定を示します。
data class Person(var id: String, val name: String) {
@PersistenceConstructor
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 にデフォルト設定されます。
7. Spring Data リポジトリの操作
Spring Data リポジトリの抽象化のゴールは、さまざまな永続ストアのデータアクセスレイヤーを実装するために必要な定型コードの量を大幅に削減することです。
Spring Data リポジトリのドキュメントとモジュール この章では、Spring Data リポジトリのコアコンセプトとインターフェースについて説明します。この章の情報は、Spring Data Commons モジュールから取得されます。Java Persistence API(JPA)モジュールの構成とコードサンプルを使用します。XML 名前空間宣言と型を、使用する特定のモジュールと同等のものに拡張するように適合させる必要があります。"[repositories.namespace-reference]" は、リポジトリ API をサポートするすべての Spring Data モジュールでサポートされる XML 構成を対象としています。"付録 A" は、リポジトリの抽象化で一般的にサポートされているクエリメソッドのキーワードをカバーしています。モジュールの特定の機能の詳細については、このドキュメントのそのモジュールの章を参照してください。 |
7.1. コアコンセプト
Spring Data リポジトリ抽象化の中心的なインターフェースは Repository です。管理するドメインクラスと、型引数としてのドメインクラスの ID 型が必要です。このインターフェースは、主に、使用する型をキャプチャーし、このインターフェースを継承するインターフェースを見つけるのに役立つマーカーインターフェースとして機能します。CrudRepository (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 のエンティティが存在するかどうかを示します。 |
また、JpaRepository や MongoRepository などの永続化技術固有の抽象化も提供します。これらのインターフェースは CrudRepository を継承し、CrudRepository などのかなり汎用的な永続化テクノロジーにとらわれないインターフェースに加えて、基礎となる永続化テクノロジーの機能を公開します。 |
CrudRepository に加えて、エンティティへのページ付けされたアクセスを容易にする追加のメソッドを追加する PagingAndSortingRepository (Javadoc) 抽象化があります。
PagingAndSortingRepository インターフェース public interface PagingAndSortingRepository<T, ID> extends CrudRepository<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);
}7.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 をセットアップして、これらのインターフェースのプロキシインスタンスを作成します。
Java 構成を使用するには、次のようなクラスを作成します。
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @EnableJpaRepositories class Config { … }XML 構成を使用するには、次のような Bean を定義します。
<?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"> <jpa:repositories base-package="com.acme.repositories"/> </beans>この例では、JPA 名前空間が使用されています。リポジトリの抽象化を他のストアに使用する場合、これをストアモジュールの適切なネームスペース宣言に変更する必要があります。つまり、たとえば
mongodbを優先してjpaを交換する必要があります。また、アノテーション付きクラスのパッケージがデフォルトで使用されるため、JavaConfig バリアントはパッケージを明示的に構成しないことに注意してください。スキャンするパッケージをカスタマイズするには、データストア固有のリポジトリの
@Enable${store}Repositories-annotation のbasePackage …属性の 1 つを使用します。
次の例に示すように、リポジトリインスタンスを挿入して使用します。
class SomeClient { private final PersonRepository repository; SomeClient(PersonRepository repository) { this.repository = repository; } void doSomething() { List<Person> persons = repository.findByLastname("Matthews"); } }
以下のセクションでは、各ステップについて詳しく説明します。
7.3. リポジトリインターフェースの定義
リポジトリインターフェースを定義するには、最初にドメインクラス固有のリポジトリインターフェースを定義する必要があります。インターフェースは Repository を継承し、ドメインクラスと ID 型に入力する必要があります。そのドメイン型の CRUD メソッドを公開する場合は、Repository ではなく CrudRepository を継承します。
7.3.1. リポジトリ定義の微調整
通常、リポジトリインターフェースは Repository、CrudRepository、PagingAndSortingRepository を継承します。または、Spring Data インターフェースを継承したくない場合は、リポジトリインターフェースに @RepositoryDefinition アノテーションを付けることもできます。CrudRepository を継承すると、エンティティを操作するためのメソッドの完全なセットが公開されます。公開するメソッドを選択したい場合は、公開するメソッドを CrudRepository からドメインリポジトリにコピーします。
| そうすることで、提供された 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 が実行時にインスタンスを作成してはならないすべてのリポジトリインターフェースに、そのアノテーションを必ず追加してください。 |
7.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 { … }7.4. クエリメソッドの定義
リポジトリプロキシには、メソッド名からストア固有のクエリを派生させる 2 つの方法があります。
メソッド名から直接クエリを導出します。
手動で定義されたクエリを使用します。
利用可能なオプションは、実際のストアによって異なります。ただし、作成する実際のクエリを決定する戦略が必要です。次のセクションでは、使用可能なオプションについて説明します。
7.4.1. クエリ検索戦略
クエリを解決するリポジトリインフラストラクチャでは、次の戦略を使用できます。XML 構成では、query-lookup-strategy 属性を使用して名前空間で戦略を構成できます。Java 構成の場合、Enable${store}Repositories アノテーションの queryLookupStrategy 属性を使用できます。特定のデータストアでは一部の戦略がサポートされていない場合があります。
CREATEは、クエリメソッド名からストア固有のクエリを作成しようとします。一般的なアプローチは、メソッド名から既知のプレフィックスの特定のセットを削除し、メソッドの残りを解析することです。クエリ構築の詳細については、"セクション 7.4.2" を参照してください。USE_DECLARED_QUERYは、宣言されたクエリを見つけようとし、見つからない場合は例外をスローします。クエリは、どこかのアノテーションによって定義することも、他の方法で宣言することもできます。そのストアで利用可能なオプションを見つけるには、特定のストアのドキュメントを参照してください。リポジトリインフラストラクチャがブートストラップ時にメソッドに対して宣言されたクエリを見つけられない場合、失敗します。CREATE_IF_NOT_FOUND(デフォルト)は、CREATEとUSE_DECLARED_QUERYを組み合わせたものです。最初に宣言されたクエリを検索し、宣言されたクエリが見つからない場合は、カスタムメソッド名ベースのクエリを作成します。これはデフォルトのルックアップ戦略であるため、明示的に何も構成しない場合に使用されます。メソッド名によるクエリ定義をすばやく行うだけでなく、必要に応じて宣言されたクエリを導入することにより、これらのクエリをカスタム調整することもできます。
7.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)を提供することにより、静的な順序を適用できます。動的な並べ替えをサポートするクエリメソッドを作成するには、"セクション 7.4.4" を参照してください。
7.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 命名規則に従うことを強くお勧めします(つまり、プロパティ名にアンダースコアを使用せず、代わりにキャメルケースを使用します)。
7.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 インスタンスを構築するために必要な追加のメタデータは作成されません(つまり、必要だったはずの追加のカウントクエリは発行されません)。むしろ、指定された範囲のエンティティのみを検索するようにクエリを制限します。
| クエリ全体で取得するページ数を調べるには、追加のカウントクエリをトリガーする必要があります。デフォルトでは、このクエリは実際にトリガーするクエリから派生します。 |
ページングとソート
プロパティ名を使用して、簡単な並べ替え式を定義できます。式を連結して、複数の条件を 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()));7.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" 最大エレメントの照会メソッドを表現できます。 |
7.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() { … }
}
@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)
private 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 に変換されます。
7.4.7. リポジトリメソッドの null 処理
Spring Data 2.0 以降、個々の集約インスタンスを返すリポジトリ CRUD メソッドは、Java 8 の Optional を使用して、値が存在しない可能性があることを示します。さらに、Spring Data はクエリメソッドで次のラッパー型を返すことをサポートしています。
com.google.common.base.Optionalscala.Optionio.vavr.control.Option
または、クエリメソッドは、ラッパー型をまったく使用しないことを選択できます。null を返すことにより、クエリ結果がないことが示されます。コレクション、コレクションの代替、ラッパー、ストリームを返すリポジトリメソッドは、null ではなく、対応する空の表現を返すことが保証されています。詳細については、"付録 B" を参照してください。
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)
import org.springframework.lang.Nullable;
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 を返します。 |
7.4.8. クエリ結果のストリーミング
戻り値の型として 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> をサポートしているわけではありません。 |
7.4.9. 非同期クエリ結果
Spring の非同期メソッド実行機能を使用すると、リポジトリクエリを非同期で実行できます。これは、Spring TaskExecutor に送信されたタスクで実際のクエリが発生している間、メソッドは呼び出し直後に戻ることを意味します。非同期クエリはリアクティブクエリとは異なるため、混在させないでください。リアクティブサポートの詳細については、ストア固有のドキュメントを参照してください。次の例は、いくつかの非同期クエリを示しています。
@Async
Future<User> findByFirstname(String firstname); (1)
@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
@Async
ListenableFuture<User> findOneByLastname(String lastname); (3)| 1 | 戻り値の型として java.util.concurrent.Future を使用します。 |
| 2 | 戻り値の型として Java 8 java.util.concurrent.CompletableFuture を使用します。 |
| 3 | 戻り値の型として org.springframework.util.concurrent.ListenableFuture を使用します。 |
7.5. リポジトリインスタンスの作成
このセクションでは、定義されたリポジトリインターフェースのインスタンスと Bean 定義を作成する方法について説明します。これを行う 1 つの方法は、リポジトリメカニズムをサポートする各 Spring Data モジュールに付属している Spring 名前空間を使用することですが、通常は Java 構成を使用することをお勧めします。
7.5.1. 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">
<repositories base-package="com.acme.repositories" />
</beans:beans> 上記の例では、Spring は com.acme.repositories とそのすべてのサブパッケージをスキャンして、Repository を継承するインターフェースまたはそのサブインターフェースの 1 つを探すように指示されています。見つかったインターフェースごとに、インフラストラクチャは永続化テクノロジー固有の FactoryBean を登録して、クエリメソッドの呼び出しを処理する適切なプロキシを作成します。各 Bean は、インターフェース名から派生した Bean 名で登録されるため、UserRepository のインターフェースは userRepository で登録されます。ネストされたリポジトリインターフェースの Bean 名には、囲む型名がプレフィックスとして付加されます。base-package 属性ではワイルドカードを使用できるため、スキャンしたパッケージのパターンを定義できます。
フィルターの使用
デフォルトでは、インフラストラクチャは、設定された基本パッケージにある永続化テクノロジ固有の Repository サブインターフェースを継承するすべてのインターフェースを取得し、そのための Bean インスタンスを作成します。ただし、Bean インスタンスが作成されているインターフェースをよりきめ細かく制御したい場合があります。これを行うには、<repositories /> 要素内で <include-filter /> 要素と <exclude-filter /> 要素を使用します。セマンティクスは、Spring のコンテキスト名前空間の要素とまったく同じです。詳細については、これらの要素の Spring リファレンスドキュメントを参照してください。
例: 特定のインターフェースをリポジトリ Bean としてインスタンス化から除外するには、次の構成を使用できます。
<repositories base-package="com.acme.repositories">
<context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories> 上記の例は、SomeRepository で終わるすべてのインターフェースをインスタンス化から除外します。
7.5.2. Java 構成
Java 構成クラスでストア固有の @Enable${store}Repositories アノテーションを使用して、リポジトリインフラストラクチャーをトリガーすることもできます。Spring コンテナーの Java ベースの構成の概要については、Spring リファレンスドキュメントの JavaConfig を参照してください。
Spring Data リポジトリを有効にするサンプル構成は次のようになります。
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
} 上記の例では、JPA 固有のアノテーションを使用しています。これは、実際に使用するストアモジュールに応じて変更します。同じことが EntityManagerFactory Bean の定義にも当てはまります。ストア固有の構成について説明しているセクションを参照してください。 |
7.5.3. スタンドアロンの使用箇所
Spring コンテナーの外部(CDI 環境など)でリポジトリインフラストラクチャを使用することもできます。クラスパスにはまだいくつかの Spring ライブラリが必要ですが、通常は、プログラムでリポジトリを設定することもできます。リポジトリサポートを提供する Spring Data モジュールには、次のように使用できる永続化テクノロジ固有の RepositoryFactory が付属しています。
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);7.6. Spring Data リポジトリのカスタム実装
Spring Data は、コーディングをほとんど行わずにクエリメソッドを作成するためのさまざまなオプションを提供します。ただし、これらのオプションがニーズに合わない場合は、リポジトリメソッドの独自のカスタム実装を提供することもできます。このセクションでは、その方法について説明します。
7.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> {
}構成
名前空間構成を使用する場合、リポジトリインフラストラクチャは、リポジトリが見つかったパッケージのクラスをスキャンすることにより、カスタム実装フラグメントを自動検出しようとします。これらのクラスは、名前空間要素の repository-impl-postfix 属性をフラグメントインターフェース名に追加する命名規則に従う必要があります。この接尾辞のデフォルトは Impl です。次の例は、デフォルトの接尾辞を使用するリポジトリと、接尾辞のカスタム値を設定するリポジトリを示しています。
<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 定義を手動で作成する代わりに、名前で手動で定義したものを参照します。次の例は、カスタム実装を手動で接続する方法を示しています。
<repositories base-package="com.acme.repository" />
<beans:bean id="userRepositoryImpl" class="…">
<!-- further configuration -->
</beans:bean>7.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 インフラストラクチャーにカスタマイズされたリポジトリ基本クラスを認識させることです。Java 構成では、次の例に示すように、@Enable${store}Repositories アノテーションの repositoryBaseClass 属性を使用してこれを行うことができます。
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }次の例に示すように、XML 名前空間で対応する属性を使用できます。
<repositories base-package="com.acme.repository"
base-class="….MyRepositoryImpl" />7.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(…) メソッドのいずれかが呼び出されるたびに呼び出されます。
7.8. Spring Data 拡張
このセクションでは、さまざまなコンテキストで Spring Data を使用できるようにする一連の Spring Data 拡張について説明します。現在、ほとんどの統合は Spring MVC を対象としています。
7.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);7.8.2. Web サポート
リポジトリプログラミングモデルをサポートする Spring Data モジュールには、さまざまな Web サポートが付属しています。Web 関連のコンポーネントでは、Spring MVC JAR がクラスパス上にある必要があります。それらのいくつかは、Spring HATEOAS [GitHub] (英語) との統合さえ提供します。一般に、統合サポートは、次の例に示すように、JavaConfig 構成クラスで @EnableSpringDataWebSupport アノテーションを使用することで有効になります。
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}@EnableSpringDataWebSupport アノテーションは、いくつかのコンポーネントを登録します。これらについては、このセクションの後半で説明します。また、クラスパスで Spring HATEOAS を検出し、統合コンポーネント(存在する場合)も登録します。
または、XML 構成を使用する場合は、次の例に示すように、SpringDataWebConfiguration または HateoasAwareSpringDataWebConfiguration のいずれかを Spring Bean として登録します(SpringDataWebConfiguration の場合)。
<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" />基本的な Web サポート
前のセクションで示した構成は、いくつかの基本的なコンポーネントを登録します。
Spring MVC がリクエストパラメーターまたはパス変数からリポジトリ管理ドメインクラスのインスタンスを解決できるようにするセクション 7.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 アノテーションを使用してカスタマイズできます。
Pageable のハイパーメディアサポート
Spring HATEOAS には、表現モデルクラス(PagedResources)が付属しています。これにより、Page インスタンスのコンテンツを、必要な Page メタデータと、クライアントがページを簡単にナビゲートできるようにするリンクで強化できます。Page から PagedResources への変換は、PagedResourcesAssembler と呼ばれる Spring HATEOAS ResourceAssembler インターフェースの実装によって行われます。次の例は、PagedResourcesAssembler をコントローラーメソッドの引数として使用する方法を示しています。
@Controller
class PersonController {
@Autowired PersonRepository repository;
@RequestMapping(value = "/persons", method = RequestMethod.GET)
HttpEntity<PagedResources<Person>> persons(Pageable pageable,
PagedResourcesAssembler assembler) {
Page<Person> persons = repository.findAll(pageable);
return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
}
} 前の例に示すように、構成を有効にすると、PagedResourcesAssembler をコントローラーメソッドの引数として使用できます。その上で toResources(…) を呼び出すと、次の効果があります。
Pageのコンテンツは、PagedResourcesインスタンスのコンテンツになります。PagedResourcesオブジェクトはPageMetadataインスタンスをアタッチし、Pageおよび基礎となるPageRequestからの情報が取り込まれます。PagedResourcesには、ページの状態に応じて、prevおよびnextリンクが添付される場合があります。リンクは、メソッドがマップする URI を指します。メソッドに追加されたページネーションパラメーターは、PageableHandlerMethodArgumentResolverの設定と一致して、リンクを後で解決できるようにします。
データベースに 30 個の Person インスタンスがあると仮定します。これで、リクエスト(GET http://localhost:8080/persons)をトリガーして、次のような出力を確認できます。
{ "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
}
} アセンブラーは正しい URI を生成し、デフォルト構成を選択して、パラメーターを次のリクエストの Pageable に解決しました。つまり、その構成を変更すると、リンクは自動的に変更に準拠します。デフォルトでは、アセンブラーはそれが呼び出されたコントローラーメソッドを指しますが、ページネーションリンクを構築するためのベースとして使用されるカスタム Link を渡すことにより、それをカスタマイズできます。これにより、PagedResourcesAssembler.toResource(…) メソッドがオーバーロードされます。
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 射影(第 8 章で説明)を使用して、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 も試行します。こうすることで、クライアントが公開メソッドを呼び出さなくても、ソースドキュメントの構造の変更を簡単に軽減できます (通常、クラスベースのペイロードバインディングの欠点です)。
ネストされた射影は、第 8 章に従ってサポートされます。メソッドがインターフェース以外の複雑な型を返す場合、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 を登録できます。 |
7.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>7.9. 例示による問い合わせ
7.9.1. 導入
この章では、Query by Example の概要とその使用方法について説明します。
Query by Example(QBE)は、シンプルなインターフェースを備えた使いやすいクエリ手法です。動的なクエリの作成が可能になり、フィールド名を含むクエリを作成する必要がなくなります。実際、Query by Example では、ストア固有のクエリ言語を使用してクエリを記述する必要はまったくありません。
7.9.2. 使用方法
サンプル API によるクエリは、3 つの部分で構成されています。
プローブ: フィールドが設定されたドメインオブジェクトの実際の例。
ExampleMatcher:ExampleMatcherには、特定のフィールドの照合方法に関する詳細が記載されています。複数の例で再利用できます。Example:Exampleは、プローブとExampleMatcherで構成されています。クエリの作成に使用されます。
例示による問い合わせは、いくつかのユースケースに適しています。
静的または動的な制約のセットを使用してデータストアをクエリします。
既存のクエリを壊すことを心配せずにドメインオブジェクトを頻繁にリファクタリングします。
基礎となるデータストア 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、…)を使用するプロパティは、プロパティパスを無視しない限り、常に含まれます。 |
例は、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.
}7.9.3. マッチャーの例
例はデフォルト設定に限定されません。次の例に示すように、ExampleMatcher を使用して、文字列照合、null 処理、プロパティ固有の設定に独自のデフォルトを指定できます。
Person person = new Person(); (1)
person.setFirstname("Dave"); (2)
ExampleMatcher matcher = ExampleMatcher.matching() (3)
.withIgnorePaths("lastname") (4)
.withIncludeNullValues() (5)
.withStringMatcherEnding(); (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 |
7.10. Spring Data Neo4j Extensions
7.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 7.9.
Additional mixins provided are:
-
QuerydslPredicateExecutor CypherdslConditionExecutorCypherdslStatementExecutorReactiveCypherdslStatementExecutor
生成されたクエリに動的条件を追加する
QuerydslPredicateExecutor と CypherdslConditionExecutor はどちらも同じ概念を提供します。つまり、SDN がクエリを生成し、追加される「述語」(クエリ DSL) または「条件」(Cypher DSL) を指定します。SDN 6 がネイティブで使用するのは Cypher DSL であるため、これをお勧めします。静的メタモデルを生成するアノテーションプロセッサー (英語) の使用も検討してください。
それはどのように機能するのでしょうか ? 上で説明したようにリポジトリを宣言し、次のインターフェースのいずれかを追加します。
interface QueryDSLPersonRepository extends
Neo4jRepository<Person, Long>, (1)
QuerydslPredicateExecutor<Person> { (2)
}| 1 | 標準リポジトリ宣言 |
| 2 | クエリ DSL ミックスイン |
OR
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
interface PersonRepository extends
Neo4jRepository<Person, Long>, (1)
CypherdslConditionExecutor<Person> { (2)
}| 1 | 標準リポジトリ宣言 |
| 2 | Cypher DSL ミックスイン |
Cypher DSL 条件エグゼキュータを使用した使用例を示します。
Node person = Cypher.node("Person").named(Constants.NAME_OF_ROOT_NODE); (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 ステートメントの使用
Adding the corresponding mixin is not different than using the condition excecutor:
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 ステートメントエグゼキューターは、ページングされた結果を返すオーバーロードも提供します。
8. 射影
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 はこれを達成するためにどのような意味を持っていますか? この章の残りはその質問に回答します。
8.1. インターフェースベースの射影
クエリの結果を名前属性のみに制限する最も簡単な方法は、次の例に示すように、読み取るプロパティのアクセサーメソッドを公開するインターフェースを宣言することです。
interface NamesOnly {
String getFirstname();
String getLastname();
}ここで重要なことは、ここで定義されたプロパティが集約ルートのプロパティと正確に一致することです。これにより、クエリメソッドを次のように追加できます。
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}クエリ実行エンジンは、返された各要素に対して実行時にそのインターフェースのプロキシインスタンスを作成し、公開されたメソッドへの呼び出しをターゲットオブジェクトに転送します。
射影は再帰的に使用できます。Address 情報の一部も含めたい場合は、次の例に示すように、そのための射影インターフェースを作成し、getAddress() の宣言からそのインターフェースを返します。
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
} メソッドの呼び出し時に、ターゲットインスタンスの address プロパティが取得され、順番に投影プロキシにラップされます。
8.1.1. 閉じた射影
アクセサーメソッドがすべてターゲット集合体のプロパティに一致する射影インターフェースは、閉じた射影と見なされます。次の例(この章の前半でも使用しました)は、閉じた射影です。
interface NamesOnly {
String getFirstname();
String getLastname();
}閉じた射影を使用する場合、Spring Data はクエリの実行を最適化できます。これは、射影プロキシのバックアップに必要なすべての属性がわかっているためです。詳細については、リファレンスドキュメントのモジュール固有の部分を参照してください。
8.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 を使用し、式でメソッドを呼び出す必要があります。
8.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 メソッドは使用されたラッパー型の空の表現を返します。
8.2. クラスベースの射影 (DTO)
射影を定義するもう 1 つの方法は、取得することになっているフィールドのプロパティを保持する値型 DTO(データ転送オブジェクト)を使用することです。これらの DTO 型は、プロキシが発生せず、ネストされた射影を適用できないことを除いて、射影インターフェースとまったく同じ方法で使用できます。
ストアがロードするフィールドを制限することでクエリの実行を最適化する場合、ロードされるフィールドは公開されているコンストラクターのパラメーター名から決定されます。
次の例は、投影 DTO を示しています。
class NamesOnly {
private final String firstname, lastname;
NamesOnly(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
String getFirstname() {
return this.firstname;
}
String getLastname() {
return this.lastname;
}
// equals(…) and hashCode() implementations
} 射影 DTO の定型コードを避ける
フィールドはデフォルトで |
8.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);
}8.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 で定義) または既知の永続エンティティにすることができます。それらのコレクションはサポートされていますが、マップはサポートされていません。
8.5. 完全な例
次のエンティティ、射影、対応するリポジトリがあるとします。
@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> findAllExtendedEntites(); (3)
List<TestEntityInterfaceProjection> findAllInterfaceProjections(); (4)
List<TestEntityDTOProjection> findAllDTOProjections(); (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 | このメソッドはインターフェース射影を返すため、メソッドの戻り値の型はリポジトリのドメインの型とは異なります。インターフェースは、ドメイン型で定義されたプロパティのみにアクセスできます。 |
| 5 | このメソッドは DTO 射影を返します。これを実行すると、DTO がドメイン型の契約にない追加属性として numberOfRelations を定義するため、SDN によって警告が発行されます。TestEntity 内のアノテーション付き属性 aProperty は、クエリ内で a_property に正しく変換されます。上記のように、戻り値の型はリポジトリのドメインの型とは異なります。 |
| 6 | このメソッドは、DTO 射影も返します。ただし、クエリには射影で定義された追加属性に適した値が含まれているため、警告は発行されません。 |
| While the repository in the listing above uses a concrete return type to define the projection, another variant is the use of dynamic projections as explained in the parts of the documentation Spring Data Neo4j shares with other Spring Data Projects. A dynamic projection can be applied to both closed and open interface projections as well as to class based DTO projections: The key to a dynamic projection is to specifiy the desired projection type as the last parameter to a query method in a repository like this: <T> Collection<T> findByName(String name, Class<T> type). This is a declaration that could be added to the TestRepository above and allow for different projections retrieved by the same method, without to repeat a possible @Query annotation on several methods. |
9. テスト
9.1. Spring Boot なし
独自の統合テストの構成のために抽象基本クラスを頻繁に使用します。それらは次のように使用できます。
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@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)
}
}
}ここで、テストサーバーまたはコンテナーへの接続を提供する必要があります。
同様のクラスがリアクティブテスト用に提供されています。
9.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.0 と Testcontainers Neo4j の 3 つのシナリオを用意しています。テストハーネスはバージョン間で変更されているため、3.5 および 4.0 には異なる例を用意しています。また、4.0 には JDK 11 が必要です。
9.2.1. @DataNeo4jTest Neo4j テストハーネス 3.5 を使用する場合
Listing 25 を実行するには、次の依存関係が必要です。
<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 および適切なリポジトリ構成で利用できます。
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Optional;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.neo4j.harness.ServerControls;
import org.neo4j.harness.TestServerBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
@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 をシャットダウンします。 |
9.2.2. Neo4j テストハーネス付き @DataNeo4jTest 4.0+
Listing 27 を実行するには、次の依存関係が必要です。
<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>{neo4j-version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
</exclusion>
</exclusions>
</dependency>Neo4j 4.x のエンタープライズバージョンの依存関係は、com.neo4j.test:neo4j-harness-enterprise および適切なリポジトリ構成で利用できます。
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Optional;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.neo4j.harness.Neo4j;
import org.neo4j.harness.Neo4jBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
@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 をシャットダウンします。 |
9.2.3. @DataNeo4jTest とテストコンテナー Neo4j
もちろん、リスト 28 で示されているように、接続構成の原則は Testcontainers でも同じです。次の依存関係が必要です。
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<version>1.14.3</version>
<scope>test</scope>
</dependency>そして完全なテスト:
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Optional;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.Neo4jContainer;
@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);
}
}9.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());
}
}
}10. 監査
10.1. 基本
Spring Data は、エンティティを作成または変更したユーザーと変更がいつ発生したかを透過的に追跡するための高度なサポートを提供します。この機能を利用するには、アノテーションを使用するか、インターフェースを実装することで定義できる監査メタデータをエンティティクラスに装備する必要があります。さらに、必要なインフラストラクチャコンポーネントを登録するには、アノテーション構成または XML 構成のいずれかを介して監査を有効にする必要があります。構成サンプルについては、ストア固有のセクションを参照してください。
作成日と変更日のみを追跡するアプリケーションでは、セクション 10.1.3 を指定する必要はありません。 |
10.1.1. アノテーションベースの監査メタデータ
エンティティを作成または変更したユーザーをキャプチャーする @CreatedBy と @LastModifiedBy、および変更が発生したときにキャプチャーする @CreatedDate と @LastModifiedDate を提供します。
class Customer {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
// … further properties omitted
} ご覧のとおり、どの情報をキャプチャーするかに応じて、アノテーションを選択的に適用できます。変更が行われたときにキャプチャーするアノテーションは、型 Joda-Time、DateTime、レガシー Java Date および Calendar、JDK8 の日付と時刻型、long または Long のプロパティで使用できます。
監査メタデータは、必ずしもルートレベルのエンティティに存在する必要はありませんが、以下に示すように、埋め込まれたエンティティに追加できます(実際に使用されているストアによって異なります)。
class Customer {
private AuditMetadata auditingMetadata;
// … further properties omitted
}
class AuditMetadata {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
}10.1.2. インターフェースベースの監査メタデータ
アノテーションを使用して監査メタデータを定義したくない場合は、ドメインクラスに Auditable インターフェースを実装させることができます。すべての監査プロパティの setter メソッドを公開します。
また、便利な基本クラス AbstractAuditable もあります。これは、インターフェースメソッドを手動で実装する必要を回避するために拡張できます。これにより、ドメインクラスの Spring Data への結合が増加しますが、これは避けたい場合があります。通常、監査メタデータを定義するアノテーションベースのメソッドは、侵襲性が低く、柔軟性が高いため好まれます。
10.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 に基づいて、どこからでも検索できると想定しています。
10.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 に基づいて、どこからでも検索できると想定しています。
11. よくある質問
序文に記載されている質問に加えて、よくある質問をいくつかご紹介します。
11.1. Neo4j 4.0 は複数のデータベースをサポートしています - どのように使用できますか ?
データベース名を静的に構成することも、独自のデータベース名プロバイダーを実行することもできます。SDN はデータベースを作成しないことに注意してください。これは、移行ツール [GitHub] (英語) を使用して行うことも、もちろん事前に簡単なスクリプトを使用して行うこともできます。
11.1.1. 静的に構成された
Spring Boot 構成で使用するデータベース名を次のように構成します (もちろん、同じプロパティが YML または環境ベースの構成に適用され、Spring Boot の規則が適用されます)。
spring.data.neo4j.database = yourDatabase この構成を適用すると、SDN リポジトリのすべてのインスタンス (リアクティブと命令の両方) および ReactiveNeo4jTemplate および Neo4jTemplate によって生成されたすべてのクエリが、データベース yourDatabase に対して実行されます。
11.1.2. 動的に構成
Spring アプリケーションの型に応じて、Bean に型 Neo4jDatabaseNameProvider または ReactiveDatabaseSelectionProvider を指定します。
Bean は、たとえば Spring のセキュリティコンテキストを使用してテナントを取得できます。以下は、Spring Security で保護された命令型アプリケーションの実例です。
import org.neo4j.springframework.data.core.DatabaseSelection;
import org.neo4j.springframework.data.core.DatabaseSelectionProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
@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);
}
}| あるデータベースから取得したエンティティを別のデータベースと混同しないように注意してください。データベース名は新しいトランザクションごとにリクエストされるため、呼び出しの間にデータベース名を変更すると、エンティティの数が予想よりも少なくなったり、多くなったりする可能性があります。さらに悪いことに、必然的に間違ったエンティティを間違ったデータベースに保存する可能性があります。 |
11.1.3. Spring Boot Neo4j ヘルスインジケーターはデフォルトのデータベースをターゲットにしていますが、これを変更するにはどうすればよいですか ?
Spring Boot には、命令型とリアクティブ型の両方の Neo4j ヘルスインジケーターが付属しています。どちらのバリアントも、アプリケーションコンテキスト内で org.neo4j.driver.Driver の複数の Bean を検出でき、各インスタンスの全体的な健全性に貢献します。ただし、Neo4j ドライバーはサーバーに接続しますが、サーバー内の特定のデータベースには接続しません。Spring Boot は Spring Data Neo4j なしでドライバーを構成できます。また、どのデータベースが使用されるかという情報は Spring Data Neo4j に関連付けられているため、この情報は組み込みのヘルスインジケーターでは利用できません。
これは、多くの デプロイシナリオでは問題にならない可能性が高くなります。ただし、構成されたデータベースユーザーに少なくともデフォルトデータベースへのアクセス権がない場合、ヘルスチェックは失敗します。
これは、データベースの選択を認識しているカスタム Neo4j ヘルスコントリビューターによって軽減できます。
命令型の変形
import java.util.Optional;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Result;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.summary.DatabaseInfo;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.summary.ServerInfo;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.util.StringUtils;
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 が接続が正常かどうかを確認するために実行するのと同じクエリを実行します。これを適用するには、次の構成を使用します。
import java.util.Map;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
@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 | これにより、個々の投稿者が正常性エンドポイントに直接表示されなくなります。 |
リアクティブバリアント
リアクティブバリアントは基本的に同じで、リアクティブ型と対応するリアクティブインフラストラクチャクラスを使用します。
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import org.neo4j.driver.Driver;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.reactive.RxResult;
import org.neo4j.driver.reactive.RxSession;
import org.neo4j.driver.summary.DatabaseInfo;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.summary.ServerInfo;
import org.reactivestreams.Publisher;
import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
import org.springframework.util.StringUtils;
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 つの異なるレジストリクリーナーが必要です。
import java.util.Map;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
import org.springframework.boot.actuate.health.HealthContributorNameFactory;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.ReactiveHealthContributor;
import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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);
}
}11.2. トランザクションが Neo4j Causal Cluster でシームレスに動作するには、特定の構成が必要ですか ?
いいえ、そうではありません。SDN は、ユーザー側での構成を必要とせずに、Neo4j Causal Cluster ブックマークを内部的に使用します。同じスレッド内のトランザクション、または相互に続く同じリアクティブストリーム内のトランザクションは、予想どおり、以前に変更された値を読み取ることができます。
11.3. 最新のブックマークを取得したり、トランザクションマネージャーをシードしたりできますか ?
ブックマーク管理で簡単に説明したように、ブックマークに関しては何も設定する必要はありません。ただし、SDN トランザクションシステムがデータベースから受信した最新のブックマークを取得すると便利な場合があります。これを行うには、BookmarkCapture のような @Bean を追加できます。
import java.util.Set;
import org.neo4j.driver.Bookmark;
import org.springframework.context.ApplicationListener;
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();
}
}トランザクションシステムをシードするには、次のようなカスタマイズされたトランザクションマネージャーが必要です。
import java.util.Set;
import java.util.function.Supplier;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.transaction.PlatformTransactionManager;
@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 | カスタマイズされたトランザクションマネージャーに渡します |
| アプリケーションがこのデータにアクセスしたり、提供したりする必要がない限り、上記のいずれのことも行う必要はありません。疑わしい場合は、どちらも行わないでください。 |
11.4. Neo4j 固有のアノテーションを使用する必要がありますか ?
いいえ。次の同等の Spring Data アノテーションを自由に使用できます。
| SDN 固有のアノテーション | Spring Data 共通アノテーション | 目的 | 相違 |
|---|---|---|---|
|
| アノテーション付きの属性を一意の ID としてマークします。 | 特定のアノテーションには追加機能はありません。 |
|
| クラスを永続エンティティとしてマークします。 |
|
11.5. 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.
11.6. 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. |
11.7. 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:
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collections;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.documentation.domain.PersonEntity;
import org.springframework.data.neo4j.documentation.domain.Roles;
@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:
import reactor.test.StepVerifier;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate;
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
import org.springframework.data.neo4j.documentation.domain.PersonEntity;
import org.springframework.data.neo4j.documentation.domain.Roles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@DataNeo4jTest
class ReactiveTemplateExampleTest {
@Container private static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.0");
@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.
11.8. 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 37 gives you an overview about what’s needed.
import org.springframework.data.domain.Pageable;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
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. |
11.9. 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 identifer, 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 Person and 2 nodes labeled Movie. Both can be mapped with a custom queury.
Assume there’s a node entity for both Person 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 {
@Id @GeneratedValue
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 38 for a domain class of type Person 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. |
11.10. 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 6 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 to add 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 6.
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 accomponied 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:
import org.springframework.data.neo4j.repository.Neo4jRepository;
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 concerncs above.
11.10.1. Using complex, dynamic custom queries and 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:
import static org.neo4j.cypherdsl.core.Cypher.anyNode;
import static org.neo4j.cypherdsl.core.Cypher.listWith;
import static org.neo4j.cypherdsl.core.Cypher.name;
import static org.neo4j.cypherdsl.core.Cypher.node;
import static org.neo4j.cypherdsl.core.Cypher.parameter;
import static org.neo4j.cypherdsl.core.Cypher.shortestPath;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.cypherdsl.core.NamedPath;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Statement;
class DomainResultsImpl implements DomainResults {
private final Neo4jTemplate neo4jTemplate; (1)
DomainResultsImpl(Neo4jTemplate neo4jTemplate) {
this.neo4jTemplate = neo4jTemplate;
}
@Override
public List<MovieEntity> findMoviesAlongShortestPath(PersonEntity from, PersonEntity to) {
Node p1 = node("Person").withProperties("name", parameter("person1"));
Node p2 = node("Person").withProperties("name", parameter("person2"));
NamedPath shortestPath = shortestPath("p").definedBy(
p1.relationshipBetween(p2).unbounded()
);
Expression p = shortestPath.getRequiredSymbolicName();
Statement 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 -
The can be generated in various ways
-
The
@Transactionalannotation is respected
11.10.2. Using custom queries and custom mappings
Often times a custom query indicates custom results.
Should all of those results be mapepd 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 6 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 6 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 typesystem 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 typesystem 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
.writeTransaction(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 usecase 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.
11.11. 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 7.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 forbide 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 41.
To enable this base repository for all declared repesotiries enable Neo4j repositories with: @EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class).
11.12. 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
第 10 章は、Spring Data Commons のより大きなコンテキストで監査を使用する方法の概要を示します。次のリストは、Spring Data Neo4j によって提供されるすべての構成オプションを示しています。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.domain.AuditorAware;
@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 つのコールバックをコンテキストに追加します。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.mapping.callback.BeforeBindCallback;
@Configuration
class CallbacksConfig {
@Bean
BeforeBindCallback<ThingWithAssignedId> nameChanger() {
return entity -> {
ThingWithAssignedId updatedThing = new ThingWithAssignedId(
entity.getTheId(), entity.getName() + " (Edited)");
return updatedThing;
};
}
}追加の構成は必要ありません。
11.13. 「例で検索」を使用するにはどうすればよいですか ?
「例による検索」は SDN の新機能です。エンティティをインスタンス化するか、既存のエンティティを使用します。このインスタンスを使用して、org.springframework.data.domain.Example を作成します。リポジトリが org.springframework.data.neo4j.repository.Neo4jRepository または org.springframework.data.neo4j.repository.ReactiveNeo4jRepository を継承している場合は、リスト 50 に示すように、例を使用して利用可能な 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);11.14. Spring Data Neo4j を使用するには Spring Boot が必要ですか ?
いいえ、そうではありません。Spring Boot を介した Spring の多くの側面の自動構成は多くの手動作業を軽減し、新しい Spring プロジェクトをセットアップする場合に推奨されるアプローチですが、これを使用する必要はありません。
上記のソリューションには次の依存関係が必要です。
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>6.0.2</version>
</dependency>Gradle セットアップのコーディネートも同じです。
静的または動的に別のデータベースを選択するには、セクション 11.1 に従って、型 DatabaseSelectionProvider の Bean を追加します。リアクティブシナリオの場合は、ReactiveDatabaseSelectionProvider を提供します。
11.14.1. Spring Boot を使用せずに Spring コンテキスト内で Spring Data Neo4j を使用する
必要な Bean の導入をサポートするために、2 つの抽象構成クラスが提供されています。つまり、命令型データベースアクセス用の AbstractNeo4jConfig とリアクティブバージョン用の AbstractReactiveNeo4jConfig です。これらはそれぞれ @EnableNeo4jRepositories および @EnableReactiveNeo4jRepositories で使用することを目的としています。使用例については、Listing 51 および Listing 52 を参照してください。どちらのクラスでも、ドライバーを作成する必要がある driver() をオーバーライドする必要があります。
Neo4j クライアントの命令型バージョン、テンプレートおよび命令型リポジトリのサポートを取得するには、ここに示すようなものを使用します。
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
@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 関連のリポジトリを検出します。
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig;
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@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());
}
}11.14.2. CDI 2.0 環境での Spring Data Neo4j の使用
便宜上、Neo4jCdiExtension による CDI 拡張機能を提供します。互換性のある CDI 2.0 コンテナーで実行すると、Java のサービスローダー SPI [Oracle] (英語) を通じて自動的に登録およびロードされます。
アプリケーションに導入する必要があるのは、Neo4j Java Driver を生成するアノテーション付き型だけです。
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
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 | Listing 51 のプレーン Spring と同じですが、対応する CDI インフラストラクチャのアノテーションが付けられます。 |
| 2 | これはオプションです。ただし、カスタムデータベース選択プロバイダーを実行する場合は、この Bean を修飾してはなりません。 |
たとえば Weld (英語) が提供するような SE コンテナーで実行している場合は、次のように拡張機能を有効にできます。
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import org.springframework.data.neo4j.config.Neo4jCdiExtension;
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();
}
}
}12. 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 が利用可能です。これは、converter 属性に Neo4jPersistentPropertyConverter を持ち、オプションで Neo4jPersistentPropertyConverterFactory を持つ属性に付与できるアノテーションで、前者を構築します。Neo4jPersistentPropertyConverter を実装することで、特定の型に対するすべての変換に対応できます。
ネイティブ型を使用しない 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 のほとんどの場合と同様、両方のクライアントは構成されたドライバーインスタンスに依存します。
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.springframework.data.neo4j.core.Neo4jClient;
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 データベースに対してのみリアクティブセッションを開くことができ、それより低いバージョンでは例外が発生して失敗します。
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.springframework.data.neo4j.core.ReactiveNeo4jClient;
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 回だけ指定できます。リスト 59 は、リアクティブクライアントを使用してそれを示します。
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 を使用します。
リスト 64 の自転車の所有者などの特定のドメインオブジェクトの 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;
}
}マッピング関数は、リスト 65 が示すように、クエリ内で発生する可能性のあるすべての名前付きパラメーターを入力する必要があります。
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 ドライバーがレコードを埋めるために使用した型へのアクセスを提供します。
マネージドトランザクションの使用中にドライバーと直接対話する
Neo4jClient または ReactiveNeo4jClient の独自の「クライアント」アプローチを望まない場合、または気に入らない場合は、データベースとのすべての対話をクライアントにコードに委譲させることができます。委譲後の対話は、クライアントの命令型バージョンとリアクティブ型バージョンでは若干異なります。
命令型バージョンは、コールバックとして Function<StatementRunner, Optional<T>> を受け取ります。空のオプションを返しても問題ありません。
StatementRunnerOptional<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 を受け取ります。
RxStatementRunnerMono<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 も参照してください: セクション 11.10. |
リレーションシップを伴うクエリ
デカルト積に注意してください
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
static 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
static class SomeRelation {
@Id @GeneratedValue
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、Person、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 {
@Id @GeneratedValue
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 型を持ち、それ以上の変換は必要ありません。
すべてのリレーションシップはマップのリストです。動的なリレーションシップはそれに応じて解決されます。あるエンティティが、異なる型の他のノードと同型のリレーションシップを持っている場合、すべて同じリストに表示されます。このようなマッピングが必要で、かつカスタムパラメーターも使用する必要がある場合は、それに応じて展開する必要があります。これを行う方法の一つは、相関サブクエリ(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 つの拡張機能が提供されています。カスタムクエリのリスト 37 を覚えていますか ? orderBy 拡張機能を使用すると、動的ソートを備えた Pageable をカスタムクエリに渡すことができます。
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
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 という名前を持ちます。 |
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
ラベルの参照
ノードをドメインオブジェクトにマップする方法はすでに知っています。
@Node(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"})
public class BikeNode {
@Id String id;
String name;
} このノードにはいくつかのラベルがあり、カスタムクエリで常に繰り返すとエラーが発生しやすくなります。1 つを忘れたり、型ミスをしたりする可能性があります。これを軽減するために、#{#staticLabels} という式を提供します。これはコロンで始まっていないことに注意してください。@Query アノテーションが付けられたリポジトリメソッドで使用します。
#{#staticLabels} in actionpublic 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 式を追加して物事を複雑にする必要はありません。
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 と認証のプロパティを調整する必要があります。リスト 83 を参照してください。
| 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 の両方については、第 5 章を参照してください。
これで、アノテーションを置き換える準備が整いました。
| 古い | 新規 |
|---|---|
|
|
|
|
|
|
|
|
|
|
| 交換不要、不要 |
| 交換不要、不要 |
| いくつかの 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 に影響します。その例はリスト 84 に示されています。
import org.neo4j.ogm.annotation.CompositeIndex;
import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.Index;
import org.neo4j.ogm.annotation.Required;
@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 の構築
要件
JDK8 以上 (OpenJDK (英語) または Oracle JDK (英語) のいずれかになります)
Maven 3.6.2 (Maven ラッパーを提供します。プロジェクトルートの
mvnwおよびmvnw.cmdをそれぞれ参照してください。ラッパーは適切な Maven バージョンを自動的にダウンロードします)Neo4j 3.5.+ データベース
ローカルで実行
または Testcontainers (英語) および Docker (英語) を介して間接的に
ビルドの実行
次のセクションは代替案であり、増加する労力によって大まかに分類されています。
すべてのビルドにはプロジェクトのローカルコピーが必要です。
$ git clone [email protected] (英語) :spring-projects/spring-data-neo4j.git続行する前に、ローカルにインストールされている JDK のバージョンを確認してください。出力は次のようになります。
$ java -version
java version "12.0.1" 2019-04-16
Java(TM) SE Runtime Environment (build 12.0.1+12)
Java HotSpot(TM) 64-Bit Server VM (build 12.0.1+12, mixed mode, sharing)Docker 装着時
デフォルトのイメージを使用する
Docker [Wikipedia] (英語) がインストールされていない場合は、Docker デスクトップ (英語) に進んでください。つまり、Docker は、いわゆるコンテナーで OS レベルの仮想化を使用して軽量のソフトウェアイメージを実行するのに役立つツールです。
ビルドではテストコンテナー Neo4j (英語) を使用してデータベースインスタンスを起動します。
$ ./mvnw clean verifyWindows マシンでは、次を使用します。
$ mvnw.cmd clean verify出力は同様になるはずです。
別のイメージを使用する
使用するイメージのバージョンは、次のような環境変数を通じて構成できます。
$ SDN_NEO4J_VERSION=3.5.11-enterprise SDN_NEO4J_ACCEPT_COMMERCIAL_EDITION=yes ./mvnw clean verifyここでは 3.5.11 enterprise を使用し、ライセンス契約にも同意します。
環境変数をインラインで指定しても機能しない場合は、環境変数の定義方法についてオペレーティングシステムまたはシェルのマニュアルを参照してください。
ローカルで実行されているデータベースに対して
| ローカルで実行されているデータベースに対して実行すると、その内容全体が消去されます。 |
毎回コンテナーを再起動する必要がないため、ローカルで実行されているデータベースに対するビルドが高速になります。開発中にこれを頻繁に行います。
Neo4j のコピーは、ダウンロードセンター (英語) からフリーで入手できます。
ご使用のオペレーティングシステムに該当するバージョンをダウンロードし、指示に従って起動してください。必要な手順は、データベースを起動した後にブラウザーを開いて http://localhost:7474 に移動し、デフォルトのパスワードを neo4j から好みのパスワードに変更することです。
その後、ローカル bolt URL を指定して完全なビルドを実行できます。
$ SDN_NEO4J_URL=bolt://localhost:7687 SDN_NEO4J_PASSWORD=secret ./mvnw clean verifyビルドを制御する環境変数の概要
| 名前 | デフォルト値 | 意味 |
|---|---|---|
| 3.5.6 | 使用する Neo4j docker イメージのバージョン。Neo4j Docker 公式イメージ (英語) を参照 |
| いいえ | 一部のテストでは、Neo4j のエンタープライズエディションが必要になる場合があります。当社は社内でエンタープライズエディションを構築してテストしていますが、ライセンスの受け入れを希望しない場合は、強制的にライセンスを受け入れることはありません。 |
| 未設定 | この環境を設定すると、ローカルで実行されている Neo4j インスタンスに接続できるようになります。開発中にこれをよく使用します。 |
| 未設定 |
|
ローカルインスタンスを使用するには、SDN_NEO4J_URL と SDN_NEO4J_PASSWORD の両方を設定する必要があります。 |
Checkstyle と仲間たち
現時点では、コードとテストの比率が現状のままであることを保証するための品質ゲートはありませんが、コントリビュートにテストを追加することを検討してください。
かなり穏やかな checkstyle ルールがいくつか用意されており、多かれ少なかれデフォルトの Java 書式設定ルールが強制されます。フォーマットエラーや未使用のインポートなどにより、ビルドが中断されてしまいます。
jQ アシスタント
また、アーキテクチャのいくつかの側面を検証するために、Neo4j ベースのツールである jQAssistant (英語) も使用しています。ルールは Cypher で記述されており、違反するとビルドが壊れます。
コーディング規則
ビルド中に次のルールがチェックされます。
API
API を健全かつ一貫した方法で公開することを保証します。
パブリック API または内部 API として公開するものを追跡するために @API ガーディアン [GitHub] (英語) を使用します。明確かつ簡潔にするために、これらのアノテーションの使用をインターフェース、クラス (コンストラクターを含む)、アノテーションに制限しています。
MATCH (c:Java)-[:ANNOTATED_BY]->(a)-[:OF_TYPE]->(t:Type {fqn: 'org.apiguardian.api.API'}),
(p)-[:DECLARES]->(c)
WHERE c:Member AND NOT c:Constructor
RETURN p.fqn, c.nameパブリックインターフェース、クラス、アノテーションは、内部 API またはパブリック API の一部であり、ステータスを持ちます。
MATCH (c:Java)-[:ANNOTATED_BY]->(a)-[:OF_TYPE]->(t:Type {fqn: 'org.apiguardian.api.API'}),
(a)-[:HAS]->({name: 'status'})-[:IS]->(s)
WHERE ANY (label IN labels(c) WHERE label in ['Interface', 'Class', 'Annotation'])
WITH c, trim(split(s.signature, ' ')[1]) AS status
WITH c, status,
CASE status
WHEN 'INTERNAL' THEN 'Internal'
ELSE 'Public'
END AS type
MERGE (a:Api {type: type, status: status})
MERGE (c)-[:IS_PART_OF]->(a)
RETURN c,aADR-003 を参照してください。
MATCH (c:Class)-[:IS_PART_OF]->(:Api {type: 'Internal'})
WHERE c.visibility = 'public'
AND coalesce(c.abstract, false) = false
AND NOT exists(c.final)
RETURN c.name名前を付ける
プロジェクト全体で次の命名規則が使用されます。
org.springframework.data.neo4j.MATCH
(project:Maven:Project)-[:CREATES]->(:Artifact)-[:CONTAINS]->(type:Type)
WHERE
NOT type.fqn starts with 'org.springframework.data.neo4j'
RETURN
project as Project, collect(type) as TypeWithWrongName物事の構造化
ほとんどの場合、org.springframework.data.neo4j のパッケージ構造は主要な建物部分を反映する必要があります。
schema and convertMATCH (a:Main:Artifact)
OPTIONAL MATCH (a)-[:CONTAINS]->(s:Package) WHERE s.fqn in ['org.springframework.data.neo4j.core.schema', 'org.springframework.data.neo4j.core.convert']
WITH collect(s) as allowed, a
MATCH (a)-[:CONTAINS]->(p1:Package)-[:DEPENDS_ON]->(p2:Package)<-[:CONTAINS]-(a)
WHERE p1.fqn = 'org.springframework.data.neo4j.core.mapping'
AND NOT (p2 in allowed OR (p1) -[:CONTAINS]-> (p2))
RETURN p1,p2MATCH (a:Main:Artifact)
MATCH (a)-[:CONTAINS]->(p1:Package)
WHERE p1.fqn in [
'org.springframework.data.neo4j.core.convert',
'org.springframework.data.neo4j.core.schema',
'org.springframework.data.neo4j.core.support',
'org.springframework.data.neo4j.core.transaction'
]
WITH p1, a
MATCH (p1)-[:CONTAINS]->(t:Type)
MATCH (t)-[:DEPENDS_ON]->(t2:Type)<-[:CONTAINS]-(p2:Package)<-[:CONTAINS]-(a)
WHERE t2.fqn <> 'org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty'
AND p2.fqn = 'org.springframework.data.neo4j.core.mapping'
RETURN tjQAssistant データベースへのアクセス
jQAssistant は Neo4j を使用してプロジェクトに関する情報を保存します。データベースにアクセスするには、上記のようにプロジェクトをビルドしてください。ビルドが完了したら、次のコマンドを実行します。
$ ./mvnw -pl org.springframework.data.neo4j:spring-data-neo4j jqassistant:serverhttp://localhost:7474 の標準 Neo4j ブラウザーと http://localhost:7474/jqassistant/dashboard/ の専用 jQA ダッシュボードにアクセスします。
スキャンと分析は、完全な検証を再度実行せずに個別にトリガーできます。
$ ./mvnw -pl org.springframework.data.neo4j:spring-data-neo4j jqassistant:scan@jqassistant-scan
$ ./mvnw -pl org.springframework.data.neo4j:spring-data-neo4j jqassistant:analyze@jqassistant-analyze付録
付録 A: リポジトリクエリキーワード
サポートされているクエリメソッドの件名キーワード
次の表に、述語を表現するために Spring Data リポジトリのクエリ導出メカニズムで一般的にサポートされているサブジェクトキーワードを示します。ここにリストされている一部のキーワードは特定のストアでサポートされていない可能性があるため、サポートされているキーワードの正確なリストについては、ストア固有のドキュメントを参照してください。
| キーワード | 説明 |
|---|---|
| 通常、リポジトリ型、 |
| 射影が存在し、通常は |
| 数値結果を返す射影をカウントします。 |
| 結果なし( |
| クエリ結果を結果の最初の |
| 一意のクエリを使用して、一意の結果のみを返します。その機能がサポートされているかどうかは、ストア固有のドキュメントを参照してください。このキーワードは、 |
サポートされているクエリメソッドの述語キーワードと修飾子
次の表に、Spring Data リポジトリクエリ派生メカニズムで一般的にサポートされている述語キーワードを示します。ただし、ここにリストされている一部のキーワードは特定のストアでサポートされていない可能性があるため、サポートされているキーワードの正確なリストについては、ストア固有のドキュメントを参照してください。
| 論理キーワード | キーワード表現 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
フィルター述語に加えて、次の修飾子のリストがサポートされています。
| キーワード | 説明 |
|---|---|
| 大文字と小文字を区別しない比較のために、述語キーワードとともに使用されます。 |
| すべての適切なプロパティの大文字と小文字を区別しません。クエリメソッド述語のどこかで使用されます。 |
| 静的な並べ替え順序を指定し、その後にプロパティのパスと方向を指定します(例: |
付録 B: リポジトリクエリの戻り値の型
サポートされているクエリの戻り値の型
次の表に、Spring Data リポジトリで一般的にサポートされる戻り値の型を示します。ただし、ここにリストされている一部の型は特定のストアでサポートされていない可能性があるため、サポートされる戻り値の型の正確なリストについてはストア固有のドキュメントを参照してください。
地理空間型(GeoResult、GeoResults、GeoPage など)は、地理空間クエリをサポートするデータストアでのみ使用できます。一部のストアモジュールは、独自の結果ラッパー型を定義する場合があります。 |
| 戻りの型 | 説明 |
|---|---|
| 戻り値がないことを示します。 |
プリミティブ | Java プリミティブ。 |
ラッパーの種類 | Java ラッパー型。 |
| 一意のエンティティ。クエリメソッドが最大で 1 つの結果を返すことを期待します。結果が見つからない場合、 |
|
|
|
|
|
|
| Java 8 または Guava |
| Scala または Vavr |
| Java 8 |
|
|
|
|
Vavr | Vavr コレクション型。詳細については、セクション 7.4.6.3 を参照してください。 |
|
|
| Java 8 |
|
|
| 使用可能なデータがさらにあるかどうかを示すサイズのデータチャンク。 |
| 結果の総数などの追加情報を含む |
| 参照場所までの距離などの追加情報を含む結果エントリ。 |
| 参照場所までの平均距離などの追加情報を含む |
| 参照位置までの平均距離など、 |
| リアクティブリポジトリを使用して 0 個または 1 個の要素を放出するプロジェクト Reactor |
| プロジェクト Reactor |
| リアクティブリポジトリを使用して単一の要素を放出する RxJava |
| リアクティブリポジトリを使用して 0 個または 1 個の要素を放出する RxJava |
| リアクティブリポジトリを使用してゼロ、1 つ、多くの要素を放出する RxJava |