領先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多MongoDB 的彈性 schema 允許在實體之間建立關係時使用多種模式。此外,對於許多用例來說,反正規化的資料模型(將相關資料儲存在單一文件中)可能是最佳選擇,因為所有資訊都保存在同一個位置,因此應用程式需要較少的查詢才能提取所有資料。然而,這種方法也有其缺點,例如潛在的資料重複、較大的文件以及最大文件大小。
一般來說,MongoDB 建議在嵌入的優點被重複的影響所掩蓋時,使用正規化的資料模型。在這篇部落格文章中,我們將探討使用手動參考和 DBRefs 連結文件時的不同可能性,以應對需要處理關係的情況。
DBRef 是 MongoDB 的原生元素,用於以明確的格式 { $db : …, $ref : …, $id : … }
表示對其他文件的參考,其中包含目標資料庫、集合和參考元素的 id 值的相關資訊,最適合連結到分佈在不同集合中的文件。
另一方面,手動參考的結構更簡單(僅儲存被參考文件的 id),但因此在處理混合集合參考時不那麼靈活。
設定好術語後,讓我們介紹眾所周知的領域類型,例如 Book
和 Publisher
,以及它們之間顯而易見的關係。
class Book {
private String isbn13;
private String title;
private int pages;
}
class Publisher {
private String name;
private String arconym;
private int foundationYear;
}
將 Publisher
嵌入到每個 Book
中並不是一個吸引人的選擇,因為這會導致資料重複,並對儲存和可維護性造成不必要的負擔。
class Book {
// ...
private Publisher publisher;
}
雖然這種儲存格式允許原子更新,並在查詢特定屬性時提供最大的彈性,但如下面的程式碼片段所示,重複 Publisher
資訊可能不值得付出這樣的代價。
{
"_id" : "617cfb",
"isbn13" : "978-0345503800",
"title" : "The Warded Man",
"pages" : 432,
"publisher" : {
"name" : "Del Rey Books",
"arconym" : "DRB",
"foundationYear" : 1977
}
}
將 Books
的集合嵌入到 Publisher
中也是如此,這會導致不必要的大型文件。正規化模型並使用連結的文件可以減輕這個問題。
第一步是確定關係的方向,以找出關係的哪一部分需要保留參考,如果不是兩者都需要的話。這個決定將影響我們稍後可用的查詢、儲存和查詢選項。
在這種情況下,Publisher
保留對相關 Books
的參考。這個想法是將這些參考儲存為 Publisher
文件中的陣列。
class Publisher {
// ...
@DBRef
List<Book> books;
}
在上面的程式碼片段中,books 屬性使用 @DBRef
進行註解。這建議 Spring Data 映射層將屬性的元素儲存為 MongoDB 原生 $dbref
元素,如下所示
{
"_id" : "833f7d",
"name" : "Del Rey Books",
"arconym" : "DRB",
"foundationYear" : 1977,
"books" : [
{
"$ref" : "book",
"$id" : "617cfb"
},
{
"$ref" : "book",
"$id" : "23e78f"
}
]
}
使用 @DBRef
註解讓我們可以透過不重複 Book 中的所有 Publisher
資訊來減少儲存大小,這很好。儘管如此,這種方法也有其缺點。Book
不再保留有關出版者的資訊,這可能會影響透過 Publisher
的屬性查找 Books
的查詢。從 Book
到出版者的缺乏反向參考也會影響在給定 Book
查找 Publisher
時的效能,因為我們現在必須針對 Publisher
集合發出一個查詢,該查詢將 Book.id
與出版者的 books
欄位進行比對,而不是直接前往其 *id*。此外,Publisher
中的 books
陣列使用了一個複雜的物件,該物件儲存了比必要更多的資訊,而僅使用 *id* 的手動參考就已足夠,因為所有參考物件都保存在同一個目標集合中。
幸運的是,有一些改進的方法,首先是新增一個指向 Publisher
的反向參考(例如,透過其 id
)
class Book {
// …
private String publisherId;
}
接下來,讓我們將儲存 Book
參考的集合從DBRef 切換為手動參考。顯而易見的一步是移除 @DBRef
註解,並將 List<Book>
替換為 List<String>
,如下面的程式碼片段所示
class Publisher {
// …
List<String> bookIds;
}
{
…
"bookIds" : ["617cfb", "23e78f", … ]
}
若要將新的 Book
新增到 Publisher
的 bookIds
欄位,我們可以使用以下語句。
template.update(Publisher.class)
.matching(where("id").is(publisher.id))
.apply(new Update().push("bookIds", book.id))
.first();
遵循這種方法可以最佳化儲存格式,並對領域模型和資料庫中使用的資料類型做出非常明確的聲明。然而,僅僅是 bookIds
並不能提供您在其中查找 bookIds
欄位中包含的值的集合的上下文。
從 Spring Data MongoDB 3.3.0 開始,可以使用 @DocumentReference
註解以宣告方式表示手動參考。
class Publisher {
// …
@DocumentReference
List<Book> books;
}
預設情況下,這會告知映射層提取被參考實體的 id
值進行儲存,並在讀取時載入被參考的文件本身。
{
…
"books" : ["617cfb", … ]
}
由於映射層知道文件之間的連結,因此更新語句(例如先前顯示的語句)會偵測到關聯並提取 *id* 進行儲存
template.update(Publisher.class)
.matching(where("id").is(publisher.id))
.apply(new Update().push("books", book))
.first();
此外,從 Book
到 Publisher
的反向參考也可以用這種方式建模。在這種情況下,延遲檢索出版者直到第一次存取屬性,以避免過早載入延遲,可能是有意義的
class Book {
// …
@DocumentReference(lazy=true)
private Publisher publisher;
}
透過使用宣告式連結,我們現在可以保留映射功能,同時最佳化儲存。儘管如此,在新增新的 Book
實例時,我們需要小心,因為這些實例也需要新增到 Publisher
的 books
欄位,以建立連結
template.save(newBook);
template.update(Publisher.class)
.matching(where("id").is(newBook.publisher.id))
.apply(new Update().push("books", newBook))
.first();
上面的程式碼片段很好地概述了使用文件之間的連結的非原子性,這可能需要在 交易 中執行操作。
根據應用程式的需求,反轉 Book
和 Publisher
之間的關係可能可行,以便連結元素僅儲存在 Book
文件中。這樣,您可以儲存 Books
,而無需考慮更新 Publisher
文件,正如我們在最後一個程式碼片段中看到的。為此,我們需要做兩件事。首先,我們需要告知映射層省略儲存從 Publisher
到 Book
的連結,其次,在檢索連結的 Books
時更新查詢查找。
初始部分相當容易,將額外的 @ReadOnlyPorperty
註解應用於 books
屬性。另一部分要求我們使用自訂查詢更新 @DocumentReference
註解的 lookup
屬性
class Publisher {
// …
@ReadOnlyProperty
@DocumentReference(lookup="{'publisher':?#{#self._id} }")
List<Book> books;
}
在上面的程式碼片段中,我們利用了 Spring Data 查詢剖析器中的表達式支援。透過這樣做,我們可以透過使用 #self
屬性存取原始 Publisher
文件,並且可以提取其識別碼,以便在查詢 Book
集合以尋找比對元素時使用它。
擁有一個包含嵌入式資料的單一聚合根有很多優點。然而,重要的是要了解,一旦這些優點被其他考量因素(例如儲存空間大小或可操作性)取代時,該如何對關係進行建模。我們已經看到,透過從嵌入式方法轉移到 DBRefs,再到手動參考,我們可以減少儲存空間大小。但是,我們必須處理其他問題,例如影響多個文件的變更以及有限的查詢選項。@DocumentReference
是一個強大的工具,可讓您表達和自訂文件之間的連結。您可以在我們的參考文檔中了解更多資訊。
儘管如此,在您離開之前,請始終牢記文件之間的連結需要額外的伺服器往返。因此,請確保有可用的索引來支援您的查找。 一組連結的文件會進行批量載入,並在您的應用程式記憶體中盡最大努力恢復排序。
此外,請始終問自己,哪種方法最適合您的應用程式?預設的嵌入方法是更好的解決方案嗎?您真的需要循環反向參考嗎?連結應該是延遲載入嗎?非原子更新將如何影響您的應用程式?最後,您需要執行哪些查詢?