使用 DBRef
映射框架不必将子对象内嵌存储在文档中。您也可以单独存储它们,并使用 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 或类似机制,因为对象的 List 已告知映射框架您希望建立一对多关系。当对象存储到 MongoDB 时,存储的是 DBRef 列表而非 Account 对象本身。在加载 DBRef 集合时,建议将集合类型中持有的引用限定到某个特定的 MongoDB 集合。这样可以批量加载所有引用;而指向不同 MongoDB 集合的引用则需要逐一解析。
映射框架不处理级联保存。如果您修改了被 Person 对象引用的 Account 对象,必须单独保存该 Account 对象。对 Person 对象调用 save 并不会自动保存 accounts 属性中的 Account 对象。
DBRef 也可以延迟解析。在这种情况下,实际的 Object 或引用 Collection 会在首次访问该属性时才解析。使用 @DBRef 的 lazy 属性来指定这一点。被定义为延迟加载 DBRef 且用作构造函数参数的必需属性,也会被装饰上延迟加载代理,以确保尽可能减少对数据库和网络的压力。
延迟加载的 DBRef 可能难以调试。请确保工具不会意外触发代理解析,例如通过调用 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 参数。
使用文档引用
使用 @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,值的解析将延迟到首次访问该属性时进行。 | 默认立即解析属性。 |
从 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;
}
{
"_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"
}
通过在
Book文档中存储Publisher.id,建立从Book(引用方)到Publisher(拥有方)的链接。将持有引用的属性标记为只读。这可防止随
Publisher文档存储指向各个Book的引用。使用
#self变量访问Publisher文档内的值,并据此检索具有匹配publisherId的Book。
具备上述全部要素后,就可以对实体之间的各种关联进行建模。请参阅下面这份不完整的示例列表,以感受可行的方式。
示例 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 查询操作符很有吸引力,而且这是可行的。但有几个方面需要考虑:
-
请确保已建立支持查找的索引。
-
请确保使用相同的数据类型:当使用
@Id String id和String someRef时,@DocumentReference(lookup="{'someRef':?#{#self._id} }")很容易失败,因为String @Id会经历自动 ObjectId 转换(但其他包含ObjectId.toString()的String属性不会)。引用查找使用的是结果Document中的值,在这种情况下,它会用ObjectId查询一个 String 字段,从而得不到任何结果。 -
请注意,解析需要一次服务端往返,会带来延迟,请考虑使用延迟策略。
-
文档引用集合会使用
$or操作符批量加载。
原始元素顺序会在内存中尽力恢复。只有在使用相等表达式时才能恢复顺序,使用 MongoDB 查询操作符时则无法恢复。在这种情况下,结果将按从存储中接收的顺序排列,或按所提供的@DocumentReference(sort)属性排列。
还有一些更一般性的说明:
-
您是否使用了循环引用?请自问是否真的需要它们。
-
延迟文档引用难以调试。请确保工具不会意外触发代理解析,例如通过调用
toString()。
尽管技术上可行,但请避免将通过引用根对象的属性获取的、单独的延迟加载实体保存回去。 -
不支持使用响应式基础设施读取文档引用。