更进一步
VMware 提供培训和认证,为您的进步提供强大动力。
了解更多MongoDB 灵活的模式允许在对实体之间的关系进行建模时采用多种模式。此外,对于许多用例来说,非规范化数据模型(将相关数据直接存储在单个文档中)可能是最佳选择,因为所有信息都保存在一个地方,这样应用程序只需更少的查询即可获取所有数据。然而,这种方法也有其缺点,例如潜在的数据重复、文档更大以及最大文档大小限制。
一般来说,当嵌入的优势被数据重复的影响所抵消时,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;
}
将每个 Book
中都嵌入 Publisher
并不是一个吸引人的选项,因为它会导致数据重复,并给存储和可维护性带来不必要的负担
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
}
}
在 Publisher
中嵌入 Books
集合也是如此,这会导致文档不必要地增大。规范化模型并使用链接文档可以缓解此问题。
第一步是确定关系的方向,以弄清楚关系中的哪一部分(如果不是双方)需要持有引用。这个决定将影响我们后续可用的查找、存储和查询选项。
在这种情况下,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 的信息,这可能会影响按 Publisher
的属性查询 Books
的操作。从 Book
到 publisher 缺乏反向引用也会影响查找给定 Book
对应的 Publisher
的性能,因为我们现在必须对 Publisher
集合发出一个查询,将 Book.id
与 publisher 的 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", … ]
}
要向 Publisher
的 bookIds
字段添加新的 Book
,我们可以使用以下语句。
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
的反向引用也可以通过这种方式建模。在这种情况下,最好延迟 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
时更新 lookup query。
第一部分相当容易,只需在 books
属性上应用额外的 @ReadOnlyPorperty
注解。另一部分需要我们使用自定义查询更新 @DocumentReference
注解的 lookup
属性
class Publisher {
// …
@ReadOnlyProperty
@DocumentReference(lookup="{'publisher':?#{#self._id} }")
List<Book> books;
}
在上面的代码片段中,我们利用了 Spring Data 查询解析器中的表达式支持。这样做,我们可以通过使用 #self
属性访问原始的 Publisher
文档,并提取其标识符,然后在查询 Book
集合以查找匹配元素时使用它。
拥有带有嵌入数据的单一聚合根具有许多优势。然而,一旦这些优势被其他问题(例如存储大小或可操作性)所取代,理解如何建模关系就很重要。我们已经看到,通过从嵌入式方法转向 DBRefs,再到手动引用,我们可以减小存储大小。但是,我们必须处理其他问题,例如影响多个文档的更改以及有限的查询选项。@DocumentReference
是一个强大的工具,它允许您表达和自定义文档之间的链接。您可以在我们的参考文档中了解更多信息。
尽管如此,在您离开之前,请始终记住文档之间的链接需要额外的服务器往返。因此,请确保有支持您查找的索引。链接文档的集合是批量加载的,并在应用程序内存中尽力恢复排序。
此外,总是问自己哪个最适合您的应用程序?默认的嵌入式方法是更好的解决方案吗?您真的需要循环反向引用吗?链接应该是延迟加载的吗?非原子更新将如何影响您的应用程序?最后,您需要运行哪些查询?