使用 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也可以延迟解析。在这种情况下,实际的ObjectCollection引用将在首次访问属性时解析。使用@DBReflazy属性指定此属性。作为构造函数参数定义的必需属性,也定义为延迟加载DBRef,也用延迟加载代理装饰,确保尽可能减少对数据库和网络的压力。

延迟加载的DBRef可能难以调试。确保工具不会意外地通过例如调用toString()或某些内联调试渲染来调用属性 getter 来触发代理解析。请考虑为org.springframework.data.mongodb.core.convert.DefaultDbRefResolver启用跟踪日志记录,以深入了解DBRef解析。
延迟加载可能需要类代理,而类代理可能需要访问 jdk 内部,这些内部从 Java 16+ 开始不再公开,这是由于JEP 396:默认情况下严格封装 JDK 内部。对于这些情况,请考虑回退到接口类型(例如,从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)
}
1 标记要引用的 Account 值的集合。
2 映射框架不处理级联保存,因此请确保单独持久化引用的实体。
3 将引用添加到现有实体。
4 引用的 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 内部,这些内部从 Java 16+ 开始不再公开,这是由于JEP 396:默认情况下严格封装 JDK 内部。对于这些情况,请考虑回退到接口类型(例如,从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;

}
Book 文档
{
  "_id" : 9a48e32,
  "title" : "The Warded Man",
  "author" : ["Peter V. Brett"],
  "publisher_ac" : "DR"
}
Publisher 文档
{
  "_id" : 1a23e45,
  "acronym" : "DR",
  "name" : "Del Rey",
  …
}
1 使用 acronym 字段查询 Publisher 集合中的实体。
2 延迟加载对 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"
  }
}
1 使用 acronym 字段查询 Publisher 集合中的实体。
2 查找查询的字段值占位符(如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;
}
Book 文档
{
  "_id" : 9a48e32,
  "title" : "The Warded Man",
  "author" : ["Peter V. Brett"],
  "publisherId" : 8cfb002
}
Publisher 文档
{
  "_id" : 8cfb002,
  "acronym" : "DR",
  "name" : "Del Rey"
}
1 通过在Book文档中存储Publisher.id来设置从Book(引用)到Publisher(所有者)的链接。
2 将保存引用的属性标记为只读。这将阻止将对单个Book的引用存储在Publisher文档中。
3 使用#self变量访问Publisher文档中的值,并在此检索具有匹配publisherIdBooks

有了以上所有内容,就可以对实体之间的所有关联进行建模。查看下面的非详尽示例列表,以了解可能的操作。

示例 1. 使用id字段的简单文档引用
class Entity {
  @DocumentReference
  ReferencedObject ref;
}
// entity
{
  "_id" : "8cfb002",
  "ref" : "9a48e32" (1)
}

// referenced object
{
  "_id" : "9a48e32" (1)
}
1 MongoDB 简单类型可以直接使用,无需进一步配置。
示例 2. 使用带有显式查找查询的id字段的简单文档引用
class Entity {
  @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") (1)
  ReferencedObject ref;
}
// entity
{
  "_id" : "8cfb002",
  "ref" : "9a48e32"                                        (1)
}

// referenced object
{
  "_id" : "9a48e32"
}
1 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"
}
1 用于获取引用值的键必须是写入时使用的键。
2 refKeytarget.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)
}
1 根据查找查询从/到链接文档读/写键fnln
2 使用非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)
  }
}
1 从/到引用文档读/写键_id,以便在查找查询中使用它们。
2 可以使用其键从引用文档中读取集合名称。

我们知道在查找查询中使用各种 MongoDB 查询运算符很诱人,这很好。但有一些方面需要考虑

  • 确保已建立支持查找的索引。

  • 请注意,解析需要服务器往返,从而导致延迟,请考虑使用延迟策略。

  • 使用$or运算符批量加载文档引用的集合。
    原始元素顺序在内存中尽力恢复。仅当使用相等表达式时才能恢复顺序,在使用 MongoDB 查询运算符时无法恢复。在这种情况下,结果将按从存储中接收或通过提供的@DocumentReference(sort)属性接收的顺序排序。

一些更一般的说明

  • 您是否使用循环引用?问问自己是否需要它们。

  • 延迟文档引用难以调试。确保工具不会意外地通过例如调用toString()来触发代理解析。

  • 不支持使用反应式基础设施读取文档引用。