メタデータベースのマッピング

SDN 内のオブジェクトマッピング機能を最大限に活用するには、マップされたオブジェクトに @Node アノテーションを付ける必要があります。マッピングフレームワークにこのアノテーションがある必要はありませんが (アノテーションがなくても、POJO は正しくマップされます)、クラスパススキャナーがドメインオブジェクトを検索して前処理し、必要なメタデータを抽出できるようになります。このアノテーションを使用しない場合、ドメインオブジェクトを初めて保存するときにアプリケーションのパフォーマンスがわずかに低下します。これは、マッピングフレームワークがドメインオブジェクトのプロパティとその保存メソッドを認識できるように内部メタデータモデルを構築する必要があるためです。持続させます。

マッピングアノテーションの概要

SDN から

  • @Node: クラスレベルで適用され、このクラスがデータベースへのマッピングの候補であることを示します。

  • @Id: ID 目的に使用されるフィールドをマークするためにフィールドレベルで適用されます。

  • @GeneratedValue@Id とともにフィールドレベルで適用され、一意の識別子の生成方法を指定します。

  • @Property: 属性からプロパティへのマッピングを変更するためにフィールドレベルで適用されます。

  • @CompositeProperty: コンポジットとして読み取られるマップ型の属性のフィールドレベルで適用されます。複合プロパティを参照してください。

  • @Relationship: フィールドレベルで適用され、関連の詳細を指定します。

  • @DynamicLabels: 動的ラベルのソースを指定するためにフィールドレベルで適用されます。

  • @RelationshipProperties: クラスレベルで適用され、このクラスが関連のプロパティのターゲットとして示されます。

  • @TargetNode@RelationshipProperties アノテーションが付けられたクラスのフィールドに適用され、相手側の観点からその関連のターゲットをマークします。

次のアノテーションは、変換を指定し、OGM との下位互換性を確保するために使用されます。

  • @DateLong

  • @DateString

  • @ConvertWith

詳細については、"変換" を参照してください。

Spring Data コモンズより

  • @org.springframework.data.annotation.Id は SDN の @Id と同じですが、実際、@Id には Spring Data Common の ID アノテーションが付けられています。

  • @CreatedBy: ノードの作成者を示すためにフィールドレベルで適用されます。

  • @CreatedDate: ノードの作成日を示すためにフィールドレベルで適用されます。

  • @LastModifiedBy: フィールドレベルで適用され、ノードに対する最後の変更の作成者を示します。

  • @LastModifiedDate: フィールドレベルで適用され、ノードの最終変更日を示します。

  • @PersistenceCreator: 1 つのコンストラクターに適用され、エンティティを読み取るときにそのコンストラクターを優先コンストラクターとしてマークします。

  • @Persistent: クラスレベルで適用され、このクラスがデータベースへのマッピングの候補であることを示します。

  • @Version: フィールドレベルで適用され、オプティミスティックロックに使用され、保存操作時に変更がチェックされます。初期値はゼロで、更新のたびに自動的に値が変更されます。

  • @ReadOnlyProperty: フィールドレベルで適用され、プロパティを読み取り専用としてマークします。プロパティはデータベースの読み取り中にハイドレートされますが、書き込みの対象にはなりません。リレーションシップで使用する場合は、関連性がなければ、そのコレクション内の関連エンティティは永続化されないことに注意してください。

監査サポートに関するすべてのアノテーションについては、監査を参照してください。

基本的な構成要素: @Node

@Node アノテーションは、マッピングコンテキストによるクラスパススキャンの対象となる、クラスをマネージドドメインクラスとしてマークするために使用されます。

オブジェクトをグラフ内のノードにマッピングしたり、その逆を行うには、マッピング先またはマッピング元のクラスを識別するラベルが必要です。

@Node には、アノテーション付きクラスのインスタンスの読み取りおよび書き込み時に使用される 1 つ以上のラベルを構成できる属性 labels があります。value 属性は labels のエイリアスです。ラベルを指定しない場合は、単純なクラス名がプライマリラベルとして使用されます。複数のラベルを指定したい場合は、次のいずれかを実行できます。

  1. labels プロパティに配列を指定します。配列内の最初の要素はプライマリラベルとみなされます。

  2. 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 このシナリオでは、インターフェースを識別するラベルを明示的に指定する必要があります
21 番目に当てはまるのは…
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 が管理対象エンティティに存在する場合、結果として得られるラベルのセットはデータベースに書き込まれる「真実」になります。

インスタンスの識別: @Id

@Node はクラスと特定のラベルを持つノード間のマッピングを作成しますが、そのクラスの個々のインスタンス (オブジェクト) とノードのインスタンス間の接続も作成する必要があります。

ここで @Id が活躍します。@Id は、クラスの属性をオブジェクトの一意の識別子としてマークします。その一意の識別子は、最適な世界では一意のビジネスキー、言い換えれば自然キーです。@Id は、サポートされている単純型のすべての属性で使用できます。

ただし、自然キーを見つけるのは非常に困難です。たとえば、人の名前は一意であることはほとんどなく、時間の経過とともに変化したり、さらに悪いことに、誰もが姓名を持っているわけではありません。

2 つの異なる種類の代理キーをサポートします。

型 Stringlong、または Long の属性では、@Id を @GeneratedValue とともに使用できます。Long と long は Neo4j 内部 ID にマップされます。String は Neo4j 5 以降で使用可能な elementId にマップされます。どちらもノードまたはリレーションシップのプロパティではなく、通常は属性には表示されませんが、SDN がクラスの個々のインスタンスを取得できるようにします。

@GeneratedValue は属性 generatorClass を提供します。generatorClass を使用して、IdGenerator を実装するクラスを指定できます。IdGenerator は関数インターフェースであり、その generateId はプライマリラベルとインスタンスを取得して ID を生成します。すぐに使える 1 つの実装として UUIDStringGenerator をサポートします。

generatorRef を介して @GeneratedValue のアプリケーションコンテキストから Spring Bean を指定することもできます。Bean も IdGenerator を実装する必要がありますが、データベースと対話するための Neo4j クライアントやテンプレートなど、コンテキスト内のすべてを利用できます。

一意の ID の処理とプロビジョニングでの ID の処理に関する重要な注意事項を無視しないでください

楽観的ロック: @Version

Spring Data Neo4j は、Long 型付きフィールドで @Version アノテーションを使用することにより、オプティミスティックロックをサポートします。この属性は更新中に自動的に増加するため、手動で変更しないでください。

たとえば、異なるスレッドの 2 つのトランザクションがバージョン x の同じオブジェクトを変更したい場合、最初の操作はデータベースに正常に永続化されます。この時点で、バージョンフィールドはインクリメントされるため、x+1 になります。2 番目の操作は、データベースに存在しないバージョン x のオブジェクトを変更しようとしているため、OptimisticLockingFailureException で失敗します。このような場合、データベースから現在のバージョンのオブジェクトを新たにフェッチすることから始めて、操作を再試行する必要があります。

ビジネス ID を使用する場合は、@Version 属性も必須です。Spring Data Neo4j はこのフィールドをチェックして、エンティティが新しいか、以前にすでに永続化されているかを判断します。

マッピングプロパティ: @Property

@Node アノテーション付きクラスのすべての属性は、Neo4j ノードおよびリレーションシップのプロパティとして保持されます。さらに構成を行わなければ、Java または Kotlin クラスの属性の名前が Neo4j プロパティとして使用されます。

既存の Neo4j スキーマを使用している場合、またはマッピングをニーズに合わせて調整したい場合は、@Property を使用する必要があります。name は、データベース内のプロパティの名前を指定するために使用されます。

ノードの接続: @Relationship

@Relationship アノテーションは、単純型ではないすべての属性で使用できます。これは、@Node のアノテーションが付けられた他の型の属性、またはそのコレクションおよびマップに適用できます。

type または value 属性では関連の型を構成でき、direction では方向を指定できます。SDN のデフォルトの方向は Relationship.Direction#OUTGOING です。

ダイナミックな関連をサポートします。動的関連は Map<String, AnnotatedDomainClass> または Map<Enum, AnnotatedDomainClass> として表されます。このような場合、他のドメインクラスとの関連の型はマップキーによって指定されるため、@Relationship を通じて構成する必要はありません。

マップ関連プロパティ

Neo4j は、ノードだけでなくリレーションシップのプロパティの定義もサポートしています。これらのプロパティをモデルで表現するために、SDN は単純な Java クラスに適用される @RelationshipProperties を提供します。プロパティクラス内には、リレーションシップが指すエンティティを定義するために、@TargetNode としてマークされたフィールドが 1 つだけ存在する必要があります。または、INCOMING 関連のコンテキストでは、から来ています。

リレーションシッププロパティクラスとその使用箇所は次のようになります。

関連プロパティ Roles
@RelationshipProperties
public class Roles {

	@RelationshipId
	private Long id;

	private final List<String> roles;

	@TargetNode
	private final PersonEntity person;

	public Roles(PersonEntity person, List<String> roles) {
		this.person = person;
		this.roles = roles;
	}


	public List<String> getRoles() {
		return roles;
	}

	@Override
	public String toString() {
		return "Roles{" +
				"id=" + id +
				'}' + this.hashCode();
	}
}

生成された内部 ID (@RelationshipId) のプロパティを定義して、保存中にプロパティを失うことなくどの関連を安全に上書きできるかを SDN が判断できるようにする必要があります。SDN が内部ノード ID を格納するフィールドを見つけられない場合、起動中に失敗します。

エンティティの関連プロパティの定義
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING) (1)
private List<Roles> actorsAndRoles = new ArrayList<>();

関連クエリの備考

一般に、クエリを作成するための関連 / ホップに制限はありません。SDN は、モデル化されたノードから到達可能なグラフ全体を解析します。

つまり、関連を双方向にマッピングする、つまりエンティティの両端で関連を定義するという考えがある場合、期待以上の結果が得られる可能性があります。

ムービー俳優がいて、すべての俳優を含む特定のムービーを取得したい例を考えてみましょう。ムービーから俳優への関連が一方向的なものであれば、これは問題になりません。双方向シナリオでは、SDN は特定の movie、その俳優だけでなく、関連の定義ごとにこの俳優に対して定義された他のムービーも取得します。最悪の場合、これは単一エンティティのグラフ全体のフェッチにまでカスケードされます。

完全な例

これらすべてを組み合わせると、単純なドメインを作成できます。さまざまなロールを持つムービーや人物を使用します。

例 1: MovieEntity
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 = new ArrayList<>();

	@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 つのロールにマッピングされています。ドメインクラスは同じです。

例 2: PersonEntity
import 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 が、関連を所有する集約ルートであると見なされます。一方、すべての人物に関連付けられたムービーを選択せずに、データベースからすべての人物を抽出できるようにしたいと考えています。データベース内のすべての関連をあらゆる方向にマッピングする前に、アプリケーションのユースケースを検討してください。これは可能ですが、オブジェクトグラフ内でグラフデータベースを再構築することになる可能性がありますが、これはマッピングフレームワークの意図したものではありません。循環ドメインまたは双方向ドメインをモデル化する必要があり、グラフ全体をフェッチしたくない場合は、射影を使用してフェッチするデータの詳細な記述を定義できます。