领先一步
VMware 提供培训和认证,助您加速进步。
了解更多MongoDB 的灵活模式允许在实体之间的关系建模时采用多种模式。此外,对于许多用例,反规范化数据模型(将相关数据存储在单个文档中)可能是最佳选择,因为所有信息都保留在一个地方,应用程序只需要更少的查询即可获取所有数据。然而,这种方法也有其缺点,例如潜在的数据重复、更大的文档和最大文档大小。
一般来说,当嵌入式模型的复制带来的负面影响超过其优势时,MongoDB 建议使用规范化的数据模型。在这篇博文中,我们将探讨在需要处理关系时,使用手动引用和DBRefs链接文档的各种可能性。
DBRef 是 MongoDB 原生的元素,用于表示对其他文档的引用,具有明确的格式 { $db : …, $ref : …, $id : … },其中包含有关目标数据库、集合和引用的元素id 值的信息,最适合链接分布在不同集合中的文档。
另一方面,手动引用在结构上更简单(仅存储被引用文档的id),但在混合集合引用方面灵活性不如 DBRefs。
在确定了术语之后,让我们介绍一些众所周知的领域类型,例如 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 按 Publisher 属性进行查找的性能。从 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", … ]
}
要将新的 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 的反向引用也可以通过这种方式建模。在这种情况下,在首次访问属性之前延迟检索 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 可以是一个强大的工具,允许您表达和自定义文档之间的链接。您可以在我们的参考文档中了解更多关于它的信息。
尽管如此,在您离开之前,请始终记住,文档之间的链接需要额外的服务器往返。因此,请务必拥有支持您查找的索引。链接文档的集合被批量加载,并在应用程序内存中尽力恢复顺序。
此外,请始终问自己,对您的应用程序而言,什么才是最好的?默认的嵌入式方法是更好的解决方案吗?您真的需要循环反向引用吗?链接应该是惰性的吗?非原子更新将如何影响您的应用程序?最后,您需要运行哪些查询?