使用 DBRefs
映射框架不必将子对象嵌入文档中存储。你也可以将它们单独存储,并使用 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;
}
你不需要使用 @OneToMany
或类似的机制,因为对象列表会告诉映射框架你想要建立一对多的关系。当对象存储在 MongoDB 中时,会有一个 DBRef
列表,而不是 Account
对象本身。在加载 DBRef
集合时,建议将集合类型中的引用限制为特定的 MongoDB 集合。这样可以批量加载所有引用,而指向不同 MongoDB 集合的引用则需要逐个解析。
映射框架不处理级联保存。如果你更改了一个被 Person
对象引用的 Account
对象,你必须单独保存 Account
对象。调用 Person
对象上的 save
方法不会自动保存 accounts
属性中的 Account
对象。
DBRef
也可以延迟解析。在这种情况下,实际的 Object
或 Collection
引用会在首次访问属性时进行解析。可以使用 @DBRef
的 lazy
属性来指定这一点。定义为延迟加载的 DBRef
的必需属性,如果用作构造函数参数,也会被延迟加载代理所装饰,从而确保对数据库和网络的压力尽可能小。
延迟加载的 DBRef
可能会难以调试。请确保工具不会意外触发代理解析,例如通过调用 toString()
或某些内联调试渲染来触发属性获取器。建议启用 org.springframework.data.mongodb.core.convert.DefaultDbRefResolver
的 trace 日志记录,以深入了解 DBRef
的解析过程。
延迟加载可能需要类代理,而类代理又可能需要访问 JDK 内部的类,这些类从 Java 16+ 开始默认是未开放的,原因在于 JEP 396: Strongly Encapsulate JDK Internals by Default。对于这些情况,请考虑回退到接口类型(例如,从 ArrayList
切换到 List
)或提供所需的 --add-opens
参数。
使用文档引用
使用 @DocumentReference
提供了一种灵活的方式来引用 MongoDB 中的实体。虽然其目标与使用 DBRefs 相同,但存储表示形式不同。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
}
标记要引用的
Account
值集合。映射框架不处理级联保存,因此请确保单独持久化被引用的实体。
将引用添加到现有实体。
被引用的
Account
实体以它们的_id
值数组形式表示。
上面的示例使用了基于 _id
的查询({ '_id' : ?#{#target} }
)来获取数据,并急切地解析了关联的实体。可以通过使用 @DocumentReference
的属性来更改默认的解析行为(如下所列)。
表 1. @DocumentReference 默认值
属性 | 描述 | 默认值 |
---|---|---|
db | 用于集合查找的目标数据库名称。 | MongoDatabaseFactory.getMongoDatabase() |
collection | 目标集合的名称。 | 注解属性的域类型,或者在 Collection 类似或 Map 属性的情况下为值类型,集合名称。 |
lookup | 使用 SpEL 表达式通过 #target 作为给定源值的标记来评估占位符的单文档查找查询。Collection 类似或 Map 属性通过 $or 操作符组合各个查找。 | 基于 _id 字段的查询({ '_id' : ?#{#target} } ),使用加载的源值。 |
sort | 用于在服务器端对结果文档进行排序。 | 默认情况下不排序。Collection 类似属性的结果顺序基于所使用的查找查询尽力恢复。 |
lazy | 如果设置为 true ,则在首次访问属性时延迟解析值。 | 默认情况下急切解析属性。 |
延迟加载可能需要类代理,而类代理又可能需要访问 JDK 内部 API,这些内部 API 从 Java 16+ 开始默认不再开放,原因是 JEP 396: 默认强封装 JDK 内部 API。对于这些情况,请考虑回退到接口类型(例如,从 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;
}
{
"_id" : 9a48e32,
"title" : "The Warded Man",
"author" : ["Peter V. Brett"],
"publisher_ac" : "DR"
}
{
"_id" : 1a23e45,
"acronym" : "DR",
"name" : "Del Rey",
…
}
使用
acronym
字段来查询Publisher
集合中的实体。延迟加载对
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"
}
}
使用
acronym
字段查询Publisher
集合中的实体。查找查询的字段值占位符(如
acc
)用于形成参考文档。
同样可以使用 @ReadonlyProperty
和 @DocumentReference
的组合来建模关系型风格的一对多引用。这种方法允许在不将链接值存储在拥有文档中的情况下定义链接类型,而是将其存储在引用文档中,如下例所示。
@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;
}
{
"_id" : 9a48e32,
"title" : "The Warded Man",
"author" : ["Peter V. Brett"],
"publisherId" : 8cfb002
}
{
"_id" : 8cfb002,
"acronym" : "DR",
"name" : "Del Rey"
}
通过将
Publisher.id
存储在Book
文档中,建立从Book
(引用)到Publisher
(所有者)的链接。将持有引用的属性标记为只读。这可以防止在
Publisher
文档中存储对单个Book
的引用。使用
#self
变量访问Publisher
文档中的值,并在此检索具有匹配publisherId
的Books
。
在完成上述所有步骤后,就可以建模实体之间的各种关联了。请查看以下非详尽的示例列表,以了解可能实现的功能。
示例 1. 使用 id 字段的简单文档引用
class Entity {
@DocumentReference
ReferencedObject ref;
}
// entity
{
"_id" : "8cfb002",
"ref" : "9a48e32" 1
}
// referenced object
{
"_id" : "9a48e32" 1
}
MongoDB 的简单类型可以直接使用,无需进一步配置。
示例 2. 使用 id 字段进行简单文档引用,并带有显式查找查询
class Entity {
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") 1
ReferencedObject ref;
}
// entity
{
"_id" : "8cfb002",
"ref" : "9a48e32" 1
}
// referenced object
{
"_id" : "9a48e32"
}
target 定义了参考值本身。
示例 3. 文档引用提取 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"
}
用于获取引用值的键必须是写入时使用的键。
refKey
是target.refKey
的简写。
示例 4. 使用多个值形成查找查询的文档引用
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
}
根据查询语句,从/向关联文档中读取/写入
fn
和ln
键。使用非 id 字段来查找目标文档。
示例 5. 从目标集合读取文档引用
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
}
}
从/向参考文档中读取/写入键
_id
,以便在查找查询中使用它们。可以使用其键从参考文档中读取集合名称。
我们知道在查询中使用各种 MongoDB 查询操作符是很诱人的,这并没有问题。但有一些方面需要考虑:
-
确保有支持你查询的索引。
-
注意解析需要一次服务器往返,这会引入延迟,考虑使用延迟加载策略。
-
使用
$or
操作符批量加载文档引用集合。
原始元素顺序会在内存中尽可能恢复。只有在使用等式表达式时才可能恢复顺序,而在使用 MongoDB 查询操作符时无法恢复。在这种情况下,结果将按照它们从存储中接收的顺序或通过提供的@DocumentReference(sort)
属性进行排序。
一些更一般的建议:
-
你是否使用了循环引用?问问自己是否真的需要它们。
-
延迟加载的文档引用很难调试。确保工具不会通过例如调用
toString()
意外触发代理解析。 -
不支持使用反应式基础设施读取文档引用。