此版本仍在开发中,尚未被视为稳定版。如需最新稳定版本,请使用 Spring Data MongoDB 5.0.4spring-doc.cadn.net.cn

使用 DBRef

映射框架并不一定要将子对象以内嵌方式存储在文档中。 您也可以将它们单独存储,并使用 DBRef 来引用该文档。 当对象从 MongoDB 加载时,这些引用会被立即解析,因此您得到的映射对象看起来与将其以内嵌方式存储在顶层文档中的效果相同。spring-doc.cadn.net.cn

以下示例使用 DBRef 引用一个独立于引用它的对象而存在的特定文档(为简洁起见,两个类均以内联方式展示):spring-doc.cadn.net.cn

@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 或类似的机制,因为对象的 List 已经告诉映射框架你希望建立一对多关系。 当该对象存储在 MongoDB 中时,保存的是 DBRef 的列表,而不是 Account 对象本身。 在加载 DBRef 集合时,建议将集合类型中持有的引用限制在特定的 MongoDB 集合内。 这样可以批量加载所有引用,而指向不同 MongoDB 集合的引用则需要逐个解析。spring-doc.cadn.net.cn

映射框架不处理级联保存。 如果你修改了一个被 Account 对象引用的 Person 对象,你必须单独保存该 Account 对象。 在 save 对象上调用 Person 方法不会自动保存其 Account 属性中的 accounts 对象。

DBRef 也可以被延迟解析。 在这种情况下,实际的 Object 或引用的 Collection 会在首次访问该属性时才被解析。 使用 lazy 注解的 @DBRef 属性来指定此行为。 那些同时被定义为延迟加载 DBRef 的必需属性,并且用作构造函数参数时,也会被装饰为延迟加载代理,以确保尽可能减少对数据库和网络的压力。spring-doc.cadn.net.cn

延迟加载的 DBRef 可能难以调试。 请确保工具不会意外触发代理解析,例如调用 toString() 方法,或某些内联调试渲染调用了属性的 getter 方法。 建议为 org.springframework.data.mongodb.core.convert.DefaultDbRefResolver 启用 trace 级别日志,以深入了解 DBRef 的解析过程。
延迟加载可能需要类代理,而类代理反过来可能需要访问 JDK 内部 API。但由于 JEP 396:默认强封装 JDK 内部 API 的实施,从 Java 16+ 开始,这些内部 API 默认不再开放。 对于这些情况,请考虑回退到接口类型(例如,将 ArrayList 改为 List),或者提供所需的 --add-opens 参数。

使用文档引用

使用@DocumentReference提供了一种在 MongoDB 中灵活引用实体的方式。 虽然目标与使用数据库引用,存储表示形式不同。DBRef解析为具有固定结构的文档,如MongoDB 参考文档.
文档引用不遵循特定格式。 它们可以是字面上的任何内容:单个值、整个文档,基本上是所有可以存储在 MongoDB 中的内容。 默认情况下,映射层将使用引用的实体id用于存储和检索的值,如下面的示例所示。spring-doc.cadn.net.cn

@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 注解的属性来修改解析的默认行为(如下所列)。spring-doc.cadn.net.cn

表1. @DocumentReference 默认值
属性 描述 默认

dbspring-doc.cadn.net.cn

用于集合查找的目标数据库名称。spring-doc.cadn.net.cn

MongoDatabaseFactory.getMongoDatabase()spring-doc.cadn.net.cn

collectionspring-doc.cadn.net.cn

目标集合名称。spring-doc.cadn.net.cn

被注解属性的领域类型,或者在 Collection 类型或 Map 类型属性的情况下,其值类型所对应的集合名称。spring-doc.cadn.net.cn

lookupspring-doc.cadn.net.cn

单文档查找查询通过 SpEL 表达式解析占位符,使用 #target 作为给定源值的标记。Collection 类型或 Map 类型的属性会通过 $or 操作符组合各个单独的查找。spring-doc.cadn.net.cn

使用加载的源值进行基于 _id 字段的查询({ '_id' : ?#{#target} })。spring-doc.cadn.net.cn

sortspring-doc.cadn.net.cn

用于在服务器端对结果文档进行排序。spring-doc.cadn.net.cn

默认情况下无。 对于类似 Collection 的属性,其结果顺序会基于所使用的查询语句,在尽最大努力的基础上进行还原。spring-doc.cadn.net.cn

lazyspring-doc.cadn.net.cn

如果设置为 true,则属性的值解析将延迟到首次访问该属性时进行。spring-doc.cadn.net.cn

默认情况下,会急切地解析属性。spring-doc.cadn.net.cn

延迟加载可能需要类代理,而类代理反过来可能需要访问 JDK 内部 API。但由于 JEP 396:默认强封装 JDK 内部 API 的实施,从 Java 16+ 开始,这些内部 API 默认不再开放。 对于这些情况,请考虑回退到接口类型(例如,将 ArrayList 改为 List),或者提供所需的 --add-opens 参数。

@DocumentReference(lookup) 允许定义与 _id 字段不同的过滤查询,从而提供了一种灵活的方式来定义实体之间的引用关系,如下例所示:书籍的 Publisher(出版商)通过其缩写进行引用,而非内部的 idspring-doc.cadn.net.cn

@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 的来源。 映射层需要注册一个目标文档与 Converter 之间的 DocumentPointer,如下所示:spring-doc.cadn.net.cn

@WritingConverter
class PublisherReferenceConverter implements Converter<Publisher, DocumentPointer<String>> {

	@Override
	public DocumentPointer<String> convert(Publisher source) {
		return () -> source.getAcronym();
	}
}

如果没有提供 DocumentPointer 转换器,则可以根据给定的查询条件计算出目标引用文档。 在这种情况下,关联目标属性将按如下示例进行求值。spring-doc.cadn.net.cn

@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 来建模关系型的一对多引用。 这种方法允许建立链接关系,而无需在拥有方文档中存储链接值,而是将链接值存储在引用方文档中,如下例所示。spring-doc.cadn.net.cn

@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,建立从 Publisher.id(引用方)到 Book(拥有方)的关联。
2 将保存引用的属性标记为只读。 这可以防止在 Book 文档中存储对单个 Publisher 的引用。
3 使用 #self 变量来访问 Publisher 文档中的值,并据此检索具有匹配 BookspublisherId

在完成上述所有配置之后,就可以对实体之间各种类型的关联关系进行建模了。 请查看下面这份非详尽的示例列表,以了解可以实现哪些功能。spring-doc.cadn.net.cn

示例 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 查询操作符是很诱人的,这样做没有问题。 但有几个方面需要考虑:spring-doc.cadn.net.cn

  • 确保已建立支持您查询的索引。spring-doc.cadn.net.cn

  • 请确保使用相同的数据类型:@DocumentReference(lookup="{'someRef':?#{#self._id} }") 在使用 @Id String idString someRef 时很容易失败,因为 String @Id 会自动进行 ObjectId 转换(但其他包含 StringObjectId.toString() 属性则不会)。 引用查找使用生成的 Document 中的值,在这种情况下,它会使用一个 ObjectId 去查询一个 String 类型的字段,从而导致查不到任何结果。spring-doc.cadn.net.cn

  • 请注意,解析操作需要一次服务器往返通信,会带来延迟,请考虑采用懒加载策略。spring-doc.cadn.net.cn

  • 文档引用的集合通过批量加载方式导入$or运算符。
    原始元素顺序在内存中会尽最大努力恢复。 仅在使用等式表达式时才可能恢复顺序,而在使用 MongoDB 查询操作符时无法完成此操作。 在这种情况下,结果将按照从存储中接收的顺序或通过提供的@DocumentReference(sort)属性。spring-doc.cadn.net.cn

几点更一般的说明:spring-doc.cadn.net.cn