领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多MongoDB 的灵活模式在对实体之间关系建模时允许使用多种模式。此外,对于许多用例来说,非规范化数据模型(将相关数据直接存储在单个文档中)可能是最佳选择,因为所有信息都保存在一个位置,因此应用程序需要更少的查询即可获取所有数据。但是,这种方法也有一些缺点,例如潜在的数据重复、更大的文档以及最大文档大小。
一般来说,当嵌入的优势被重复的影响所忽略时,MongoDB 建议使用规范化数据模型。在这篇博文中,我们来看看在需要处理关系时使用手动引用和DBRef链接文档的不同可能性。
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;
}
接下来,让我们从DBRef切换到手动引用来存储Book
引用的集合。显而易见的步骤是删除@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
文档中。这使您可以在无需考虑更新Publisher
文档的情况下存储Books
,就像我们在上一段代码段中看到的那样。为此,我们需要做两件事。首先,我们需要告诉映射层省略存储从Publisher
到Book
的链接,其次,在检索链接的Books
时更新查找查询。
初始部分相当容易,只需对books
属性应用额外的@ReadOnlyPorperty
注释。另一部分要求我们使用自定义查询更新@DocumentReference
注释的lookup
属性。
class Publisher {
// …
@ReadOnlyProperty
@DocumentReference(lookup="{'publisher':?#{#self._id} }")
List<Book> books;
}
在上面的代码段中,我们利用了 Spring Data 查询解析器中的表达式支持。这样做,我们可以通过使用#self
属性访问原始Publisher
文档,并可以提取其标识符,然后在查询Book
集合以查找匹配元素时使用它。
将数据嵌入到单个聚合根中有很多优势。但是,了解如何在这些优势被其他问题(例如存储大小或可操作性)取代后建模关系非常重要。我们已经看到,通过从嵌入方法转向DBRefs,再到手动引用,我们可以减少存储大小。但是,我们必须处理其他问题,例如影响多个文档的更改和有限的查询选项。@DocumentReference
是一种强大的工具,可让您表达和自定义文档之间的链接。您可以在我们的参考文档中了解更多信息。
但是,在您离开之前,请始终牢记,文档之间的链接需要额外的服务器往返。因此,请确保有可用的索引来支持您的查找。链接文档的集合是批量加载的,在应用程序的内存中尽力恢复排序。
此外,始终问问自己哪个最适合您的应用程序?默认的嵌入方法是更好的解决方案吗?您真的需要循环反向引用吗?链接应该是延迟加载的吗?非原子更新将如何影响您的应用程序?最后,您需要运行哪些查询?