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
の属性を使用して、解決のデフォルト (以下にリスト) を変更することができます。
属性 | 説明 | デフォルト |
---|---|---|
| コレクション検索のターゲットデータベース名。 |
|
| ターゲットのコレクション名。 | アノテーションが付けられたプロパティのドメイン型、 |
| 指定されたソース値のマーカーとして | ロードされたソース値を使用する |
| サーバー側で結果ドキュメントをソートするために使用されます。 | デフォルトではなし。 |
|
| デフォルトではプロパティを積極的に解決します。 |
遅延読み込みにはクラスプロキシが必要になる場合があり、その結果、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",
…
}
1 | acronym フィールドを使用して、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"
}
}
1 | acronym フィールドを使用して、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"
}
1 | Publisher.id を Book ドキュメント内に保存することで、Book (参照) から Publisher (所有者) へのリンクを設定します。 |
2 | 参照を保持するプロパティを読み取り専用にマークします。これにより、個々の Book への参照を Publisher ドキュメントに保存できなくなります。 |
3 | #self 変数を使用して Publisher ドキュメント内の値にアクセスし、この中で publisherId と一致する Books を取得します。 |
上記をすべて準備すると、エンティティ間のあらゆる種類の関連付けをモデル化することができます。以下のサンプルのリスト (すべてではありません) を見て、何が可能なのかを感じてください。
class Entity {
@DocumentReference
ReferencedObject ref;
}
// entity
{
"_id" : "8cfb002",
"ref" : "9a48e32" (1)
}
// referenced object
{
"_id" : "9a48e32" (1)
}
1 | MongoDB のシンプルな型は、追加の設定を行わずに直接使用できます。 |
class Entity {
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") (1)
ReferencedObject ref;
}
// entity
{
"_id" : "8cfb002",
"ref" : "9a48e32" (1)
}
// referenced object
{
"_id" : "9a48e32"
}
1 | target は基準値自体を定義します。 |
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 | 参照値の取得に使用するキーは、書き込み時に使用したキーである必要があります。 |
2 | refKey は target.refKey の短縮形です。 |
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 フィールドを使用します。 |
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 クエリ演算子を使用したくなることはわかっていますが、これは問題ありません。ただし、考慮すべき点がいくつかあります。
さらにいくつかの一般的な注意事項:
|