DBRef の使用

マッピングフレームワークは、ドキュメント内に埋め込まれた子オブジェクトを保存する必要はありません。個別に保存し、DBRef を使用してそのドキュメントを参照することもできます。オブジェクトが MongoDB からロードされると、これらの参照は積極的に解決されるため、トップレベルのドキュメント内に埋め込まれて保存されているかのように見えるマップされたオブジェクトが返されます。

次の例では、DBRef を使用して、参照先のオブジェクトとは独立して存在する特定のドキュメントを参照します (簡潔にするために、両方のクラスがインラインで示されています)。

@Document
public class Account {

  @Id
  private ObjectId id;
  private Float total;
}

@Document
public class Person {

  @Id
  private ObjectId id;
  @Indexed
  private Integer ssn;
  @DBRef
  private List<Account> accounts;
}

オブジェクトのリストによってマッピングフレームワークに 1 対多の関連が必要であることが伝えられるため、@OneToMany または同様のメカニズムを使用する必要はありません。オブジェクトが MongoDB に格納されると、Account オブジェクト自体ではなく DBRef のリストが存在します。DBRef のコレクションをロードする場合は、コレクション型に保持される参照を特定の MongoDB コレクションに制限することをお勧めします。これにより、すべての参照の一括ロードが可能になりますが、異なる MongoDB コレクションを指す参照は 1 つずつ解決する必要があります。

マッピングフレームワークはカスケード保存を処理しません。Person オブジェクトによって参照される Account オブジェクトを変更する場合は、Account オブジェクトを個別に保存する必要があります。Person オブジェクトで save を呼び出しても、Account オブジェクトは accounts プロパティに自動的に保存されません。

DBRef は遅延解決することもできます。この場合、参照の実際の Object または Collection は、プロパティへの最初のアクセス時に解決されます。これを指定するには、@DBRef の lazy 属性を使用します。遅延読み込み DBRef としても定義され、コンストラクター引数として使用される必須プロパティも遅延読み込みプロキシで修飾され、データベースとネットワークへの負担をできるだけ少なくします。

遅延ロードされた DBRef はデバッグが難しい場合があります。たとえば、toString() を呼び出すか、プロパティ getter を呼び出すインラインデバッグレンダリングなど、ツールが誤ってプロキシ解決をトリガーしないようにしてください。DBRef の解決策を把握するには、org.springframework.data.mongodb.core.convert.DefaultDbRefResolver のトレースログを有効にすることを検討してください。
遅延読み込みにはクラスプロキシが必要になる場合があり、その結果、JEP 396: デフォルトで JDK 内部を強力にカプセル化する (英語) が原因で Java 16+ 以降のオープンされていない JDK 内部へのアクセスが必要になる場合があります。このような場合は、インターフェース型にフォールバックする (例: ArrayList から List に切り替える) か、必要な --add-opens 引数を指定することを検討してください。

ドキュメント参照の使用

@DocumentReference を使用すると、MongoDB 内のエンティティを参照する柔軟な方法が提供されます。ゴールは DBRef を使用する場合と同じですが、ストアの表現は異なります。DBRef は、MongoDB リファレンスドキュメント (英語) で概説されている固定構造を持つドキュメントに解決されます。
ドキュメント参照は、特定の形式に従っていません。これらは文字通り何でも、単一の値、ドキュメント全体、基本的に MongoDB に保存できるすべてのものにすることができます。デフォルトでは、マッピングレイヤーは、以下のサンプルのように、保存と取得に参照エンティティ ID 値を使用します。

@Document
class Account {

  @Id
  String id;
  Float total;
}

@Document
class Person {

  @Id
  String id;

  @DocumentReference                                   (1)
  List<Account> accounts;
}
Account account = …

template.insert(account);                               (2)

template.update(Person.class)
  .matching(where("id").is(…))
  .apply(new Update().push("accounts").value(account)) (3)
  .first();
{
  "_id" : …,
  "accounts" : [ "6509b9e" … ]                        (4)
}
1 参照する Account 値のコレクションをマークします。
2 マッピングフレームワークはカスケード保存を処理しないため、参照されたエンティティを個別に永続化するようにしてください。
3 既存のエンティティに参照を追加します。
4 参照される Account エンティティは、_id 値の配列として表されます。

上記のサンプルでは、データ取得に _id ベースのフェッチクエリ ({ '_id' : ?#{#target} }) を使用し、リンクされたエンティティを積極的に解決します。@DocumentReference の属性を使用して、解決のデフォルト (以下にリスト) を変更することができます。

表 1: @DocumentReference のデフォルト
属性 説明 デフォルト

db

コレクション検索のターゲットデータベース名。

MongoDatabaseFactory.getMongoDatabase()

collection

ターゲットのコレクション名。

アノテーションが付けられたプロパティのドメイン型、Collection のようなプロパティまたは Map プロパティの場合はそれぞれ値の型、コレクション名。

lookup

指定されたソース値のマーカーとして #target を使用し、SpEL 式を介してプレースホルダーを評価する単一のドキュメント検索クエリ。Collection のようなプロパティまたは Map プロパティは、$or 演算子を介して個々の検索を結合します。

ロードされたソース値を使用する _id フィールドベースのクエリ ({ '_id' : ?#{#target} })。

sort

サーバー側で結果ドキュメントをソートするために使用されます。

デフォルトではなし。Collection のようなプロパティの結果の順序は、使用された検索クエリに基づいてベストエフォートで復元されます。

lazy

true 値に設定すると、プロパティの最初のアクセス時に解決が遅れます。

デフォルトではプロパティを積極的に解決します。

遅延読み込みにはクラスプロキシが必要になる場合があり、その結果、JEP 396: デフォルトで JDK 内部を強力にカプセル化する (英語) が原因で Java 16+ 以降のオープンされていない JDK 内部へのアクセスが必要になる場合があります。このような場合は、インターフェース型にフォールバックする (例: ArrayList から List に切り替える) か、必要な --add-opens 引数を指定することを検討してください。

@DocumentReference(lookup) では、_id フィールドとは異なるフィルタークエリを定義できるため、以下のサンプルで示すように、エンティティ間の参照を定義する柔軟な方法が提供されます。書籍の Publisher は、内部の id ではなくその頭字語で参照されます。

@Document
class Book {

  @Id
  ObjectId id;
  String title;
  List<String> author;

  @Field("publisher_ac")
  @DocumentReference(lookup = "{ 'acronym' : ?#{#target} }") (1)
  Publisher publisher;
}

@Document
class Publisher {

  @Id
  ObjectId id;
  String acronym;                                            (1)
  String name;

  @DocumentReference(lazy = true)                            (2)
  List<Book> books;

}
Book ドキュメント
{
  "_id" : 9a48e32,
  "title" : "The Warded Man",
  "author" : ["Peter V. Brett"],
  "publisher_ac" : "DR"
}
Publisher ドキュメント
{
  "_id" : 1a23e45,
  "acronym" : "DR",
  "name" : "Del Rey",
  …
}
1acronym フィールドを使用して、Publisher コレクション内のエンティティをクエリします。
2 遅延ロードバックは、Book コレクションへの参照を返します。

上記のスニペットは、カスタム参照オブジェクトを操作するときの読み取り側を示しています。マッピング情報では #target の由来が示されていないため、書き込みには少し追加の設定が必要です。マッピング層では、次のようなターゲットドキュメントと DocumentPointer の間の Converter の登録が必要です。

@WritingConverter
class PublisherReferenceConverter implements Converter<Publisher, DocumentPointer<String>> {

	@Override
	public DocumentPointer<String> convert(Publisher source) {
		return () -> source.getAcronym();
	}
}

DocumentPointer コンバーターが提供されていない場合は、指定された検索クエリに基づいてターゲットリファレンスドキュメントを計算できます。この場合、関連付けターゲットのプロパティは次のサンプルに示すように評価されます。

@Document
class Book {

  @Id
  ObjectId id;
  String title;
  List<String> author;

  @DocumentReference(lookup = "{ 'acronym' : ?#{acc} }") (1) (2)
  Publisher publisher;
}

@Document
class Publisher {

  @Id
  ObjectId id;
  String acronym;                                        (1)
  String name;

  // ...
}
{
  "_id" : 9a48e32,
  "title" : "The Warded Man",
  "author" : ["Peter V. Brett"],
  "publisher" : {
    "acc" : "DOC"
  }
}
1acronym フィールドを使用して、Publisher コレクション内のエンティティをクエリします。
2 ルックアップクエリのフィールド値のプレースホルダー ( acc など) は、リファレンスドキュメントの形成に使用されます。

@ReadonlyProperty と @DocumentReference の組み合わせを使用して、リレーショナルスタイルの 1 対多の参照をモデル化することもできます。このアプローチでは、以下の例に示すように、リンク値を所有ドキュメント内ではなくリファレンスドキュメントに保存せずに、リンク型を使用できます。

@Document
class Book {

  @Id
  ObjectId id;
  String title;
  List<String> author;

  ObjectId publisherId;                                        (1)
}

@Document
class Publisher {

  @Id
  ObjectId id;
  String acronym;
  String name;

  @ReadOnlyProperty                                            (2)
  @DocumentReference(lookup="{'publisherId':?#{#self._id} }")  (3)
  List<Book> books;
}
Book ドキュメント
{
  "_id" : 9a48e32,
  "title" : "The Warded Man",
  "author" : ["Peter V. Brett"],
  "publisherId" : 8cfb002
}
Publisher ドキュメント
{
  "_id" : 8cfb002,
  "acronym" : "DR",
  "name" : "Del Rey"
}
1Publisher.id を Book ドキュメント内に保存することで、Book (参照) から Publisher (所有者) へのリンクを設定します。
2 参照を保持するプロパティを読み取り専用にマークします。これにより、個々の Book への参照を Publisher ドキュメントに保存できなくなります。
3#self 変数を使用して Publisher ドキュメント内の値にアクセスし、この中で publisherId と一致する Books を取得します。

上記をすべて準備すると、エンティティ間のあらゆる種類の関連付けをモデル化することができます。以下のサンプルのリスト (すべてではありません) を見て、何が可能なのかを感じてください。

例 1: id フィールドを使用した簡単なドキュメント参照
class Entity {
  @DocumentReference
  ReferencedObject ref;
}
// entity
{
  "_id" : "8cfb002",
  "ref" : "9a48e32" (1)
}

// referenced object
{
  "_id" : "9a48e32" (1)
}
1MongoDB のシンプルな型は、追加の設定を行わずに直接使用できます。
例 2: 明示的な検索クエリで id フィールドを使用した単純なドキュメント参照
class Entity {
  @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") (1)
  ReferencedObject ref;
}
// entity
{
  "_id" : "8cfb002",
  "ref" : "9a48e32"                                        (1)
}

// referenced object
{
  "_id" : "9a48e32"
}
1target は基準値自体を定義します。
例 3: ルックアップクエリの refKey フィールドを抽出するドキュメントリファレンス
class Entity {
  @DocumentReference(lookup = "{ '_id' : '?#{refKey}' }")  (1) (2)
  private ReferencedObject ref;
}
@WritingConverter
class ToDocumentPointerConverter implements Converter<ReferencedObject, DocumentPointer<Document>> {
	public DocumentPointer<Document> convert(ReferencedObject source) {
		return () -> new Document("refKey", source.id);    (1)
	}
}
// entity
{
  "_id" : "8cfb002",
  "ref" : {
    "refKey" : "9a48e32"                                   (1)
  }
}

// referenced object
{
  "_id" : "9a48e32"
}
1 参照値の取得に使用するキーは、書き込み時に使用したキーである必要があります。
2refKey は target.refKey の短縮形です。
例 4: 検索クエリを形成する複数の値を含むドキュメント参照
class Entity {
  @DocumentReference(lookup = "{ 'firstname' : '?#{fn}', 'lastname' : '?#{ln}' }") (1) (2)
  ReferencedObject ref;
}
// entity
{
  "_id" : "8cfb002",
  "ref" : {
    "fn" : "Josh",           (1)
    "ln" : "Long"            (1)
  }
}

// referenced object
{
  "_id" : "9a48e32",
  "firstname" : "Josh",      (2)
  "lastname" : "Long",       (2)
}
1 ルックアップクエリに基づいて、リンケージドキュメントからキー fn および ln を読み取り / 書き込みします。
2 ターゲットドキュメントの検索には非 ID フィールドを使用します。
例 5: ターゲットコレクションからのドキュメント参照の読み取り
class Entity {
  @DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "?#{collection}") (2)
  private ReferencedObject ref;
}
@WritingConverter
class ToDocumentPointerConverter implements Converter<ReferencedObject, DocumentPointer<Document>> {
	public DocumentPointer<Document> convert(ReferencedObject source) {
		return () -> new Document("id", source.id)                                   (1)
                           .append("collection", … );                                (2)
	}
}
// entity
{
  "_id" : "8cfb002",
  "ref" : {
    "id" : "9a48e32",                                                                (1)
    "collection" : "…"                                                               (2)
  }
}
1 キー _id をリファレンスドキュメントから読み取り / リファレンスドキュメントに書き込み、検索クエリで使用します。
2 コレクション名は、そのキーを使用してリファレンスドキュメントから読み取ることができます。

ルックアップクエリであらゆる種類の MongoDB クエリ演算子を使用したくなることはわかっていますが、これは問題ありません。ただし、考慮すべき点がいくつかあります。

  • 検索をサポートするインデックスを必ず配置してください。

  • 解決にはサーバーのラウンドトリップが必要であり、遅延が発生することに注意して、遅延戦略を検討してください。

  • ドキュメント参照のコレクションは、$or オペレーターを使用して一括ロードされます。
    元の要素の順序は、ベストエフォートベースでメモリ内に復元されます。順序の復元は等価式を使用する場合にのみ可能であり、MongoDB クエリ演算子を使用する場合は実行できません。この場合、結果はストアから受信したとき、指定された @DocumentReference(sort) 属性を介して順序付けされます。

さらにいくつかの一般的な注意事項:

  • 循環参照を使用していますか ? それらが必要かどうか自分自身に問いかけてください。

  • 遅延ドキュメント参照はデバッグが困難です。ツールが誤ってプロキシ解決をトリガーしないようにしてください。toString() を呼び出します。

  • リアクティブインフラストラクチャを使用したドキュメント参照の読み取りはサポートされていません。