Using 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 会在首次访问该属性时被解析。使用 @DBReflazy 属性来指定这一点。定义为懒惰加载 DBRef 并且用作构造函数参数的必需属性也用懒惰加载代理进行修饰,以确保尽可能减少对数据库和网络的压力。

延迟加载 DBRef`s can be hard to debug. Make sure tooling does not accidentally trigger proxy resolution by e.g. calling `toString() 或某些调用属性 getter 的内联调试呈现。请考虑为 org.springframework.data.mongodb.core.convert.DefaultDbRefResolver 启用 trace 日志记录,以便深入了解 DBRef 解析。

延迟加载可能需要类代理,而类代理反过来又可能需要访问从 Java 16+ 开始由于 JEP 396: Strongly Encapsulate JDK Internals by Default 而不再开放的 jdk 内部信息。对于这些情况,请考虑回退到一个接口类型(例如,从`ArrayList` 切换到`List`)或提供所需的`--add-opens` 参数。

Using Document References

使用 @DocumentReference 提供了一种灵活的方法来引用 MongoDB 中的实体。虽然目标与使用 DBRefs 相同,但存储表示是不同的。`DBRef`解析为具有固定结构的文档,如 MongoDB Reference documentation 中所述。文档引用不遵循特定的格式。它们可以是任何东西,单个值、整个文档,基本上可以存储在 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 的属性来更改解析默认值(如下所列)。

Table 1. @DocumentReference defaults
Attribute Description Default

db

要进行集合查找的目标数据库名称。

MongoDatabaseFactory.getMongoDatabase()

collection

The target collection name.

带注解的属性的域类型,对于 Collection 之类的属性或 Map 属性而言分别为值类型和集合名称。

lookup

使用 #target 作为给定源值的标记,通过 SpEL 表达式评估占位符的单文档查找查询。Collection 之类的属性或 Map 属性通过 $or 运算符组合各个查找。

通过使用加载的源值而进行的基于 _id 字段的查询 ({ '_id' : ?#{#target} })。

sort

在服务器端整理结果文档时使用。

默认情况下为 None。根据以最大努力为基础的对照查询,恢复 Collection 类似属性的结果顺序。

lazy

如果对 true 设置延迟,则在第一次访问属性时延迟值解析。

默认情况下急切解析属性。

延迟加载可能需要类代理,而类代理反过来又可能需要访问从 Java 16+ 开始由于 JEP 396: Strongly Encapsulate JDK Internals by Default 而不再开放的 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 document
{
  "_id" : 9a48e32,
  "title" : "The Warded Man",
  "author" : ["Peter V. Brett"],
  "publisher_ac" : "DR"
}
Publisher document
{
  "_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 document
{
  "_id" : 9a48e32,
  "title" : "The Warded Man",
  "author" : ["Peter V. Brett"],
  "publisherId" : 8cfb002
}
Publisher document
{
  "_id" : 8cfb002,
  "acronym" : "DR",
  "name" : "Del Rey"
}
1 Book (参考) 链接至 Publisher (所有者) 的方法是将 Publisher.id 存储在 Book 文档中。
2 将存储引用的属性标记为只读。这会阻止存储对各个 Book`s with the `Publisher 文档的引用。
3 使用 #self 变量来访问 Publisher 文档中的值,并使用匹配的 publisherId 检索 Books

有了上述所有内容,就可以对实体之间的所有关联进行建模。参阅下面这个不详尽的示例列表,以了解其可能性。

Example 1. Simple Document Reference using id field
class Entity {
  @DocumentReference
  ReferencedObject ref;
}
// entity
{
  "_id" : "8cfb002",
  "ref" : "9a48e32" 1
}

// referenced object
{
  "_id" : "9a48e32" 1
}
1 无需进一步的配置,即可直接使用 MongoDB 简单类型。
Example 2. Simple Document Reference using id field with explicit lookup query
class Entity {
  @DocumentReference(lookup = "{ '_id' : '?#{#target}' }") 1
  ReferencedObject ref;
}
// entity
{
  "_id" : "8cfb002",
  "ref" : "9a48e32"                                        1
}

// referenced object
{
  "_id" : "9a48e32"
}
1 target 定义引用值本身。
Example 3. Document Reference extracting the refKey field for the lookup query
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 的简称。
Example 4. Document Reference with multiple values forming the lookup query
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 字段用于查找目标文档。
Example 5. Document Reference reading from a target collection
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()

  • 不支持使用响应式基础结构读取文档引用。