在Spring Data MongoDB中实现关系建模 - spring.io


如何在 Spring Data MongoDB 中使用Manual references和 DBRefs建模关系的实用指南。

  • DBRef是 MongoDB 的本机元素,用于以显式格式表达对其他文档的引用,该格式{ $db : …, $ref : …, $id : … }保存有关目标数据库、集合和引用元素的id值的信息,最适合链接到分布在不同集合中的文档。
  • Manual references手动引用在结构上更简单(通过仅存储被引用文档的id),但因此在混合集合引用时不那么灵活。

让我们介绍众所周知的域类型,例如Bookand Publisher,以及它们之间的明显关系:
class Book {
    private String isbn13;
    private String title;
    private int pages;
}

class Publisher {
    private String name;
    private String arconym;
    private int foundationYear;
}

将每个Publisher嵌入到每个Book中并不是一个有吸引力的选择,因为它会导致数据重复并对存储和可维护性造成不必要的负担:
class Book {
    // ...
    private Publisher publisher;
}

尽管这种存储格式允许原子更新并在查询特定属性时提供最大的灵活性,但如下面的片段所示,信息的重复可能不值得付出这样的代价:
{
    "_id" : "617cfb",
   
"isbn13" : "978-0345503800",
   
"title" : "The Warded Man",
   
"pages" : 432,
   
"publisher" : {
       
"name" : "Del Rey Books",
       
"arconym" : "DRB",
       
"foundationYear" : 1977
    }
}

规范化模型并使用链接文档可以缓解这个问题。
第一步是确定关系的方向,以确定关系的哪一部分需要保存引用,如果不是两者都需要。此决定将影响我们稍后可用的查找、存储和查询选项。
 
使用DBRef 链接
Publisher持有引用关联的Books. 这个想法是将这些引用存储为Publisher文档中的数组:

class Publisher {
    // ...
    @DBRef
    List<Book> books;
}

在上面的代码片段中,books 属性用@DBRef. 这建议 Spring Data 映射层将属性的元素存储为 MongoDB 本机$dbref元素,如下所示:

{
    "_id" : "833f7d",
   
"name" : "Del Rey Books",
   
"arconym" : "DRB",
   
"foundationYear" : 1977,
   
"books" : [
        {
           
"$ref" : "book",
           
"$id" : "617cfb"
        },
        {
           
"$ref" : "book",
           
"$id" : "23e78f"
        }
    ]
}

使用@DBRef注释可以通过不重复Book 中的所有Publisher信息来减少存储大小。
尽管如此,这种方法也有其缺点。Book将不再持有Publisher信息,可能会影响按出版商Publisher属性查找书籍Book的查询。
缺少对Publisher的反向引用也会影响性能。
有一些方法可以改进,首先是添加对 Publisher 的反向引用(例如,通过它的id):

class Book {
    // …
    private String publisherId;
}

  
使用Manual References
让我们从DBRef切换到Manual References来存储引用的集合Book。显而易见的步骤是删除@DBRef注释并将 替换List<Book>为List<String>。
class Publisher {
    // …
    List<String> bookIds;
}

{
    …
    "bookIds" : ["617cfb", "23e78f", … ]
}

要添加一个新的Book到Publisher 的bookIds字段,我们可以使用以下语句。
template.update(Publisher.class)
    .matching(where("id").is(publisher.id))
    .apply(new Update().push(
"bookIds", book.id))
    .first();

遵循这种方法优化了存储格式,并对域模型和数据库中使用的数据类型做出了非常明确的声明。
然而,只有一个bookIds不会为您提供在其中查找bookIds字段中包含的值的集合的上下文。
 
使用声明性手册参考引用 (Declarative Manual References)
Spring Data MongoDB 3.3.0 开始,手动引用可以通过使用@DocumentReference注释以声明方式表示:
class Publisher {
    // …
    @DocumentReference
    List<Book> books;
}

默认情况下,这会告诉映射层提取引用实体的id值进行存储,在读取时加载引用文档本身。

{
    …
    "books" : ["617cfb", … ]
}

此外,可以通过这种方式对来自Book到Publisher的反向引用进行建模。在这种情况下,将发布者的检索延迟到第一次访问该属性以避免急切加载延迟可能是有意义的:

class Book {
    // …
    @DocumentReference(lazy=true)
    private Publisher publisher;
}

通过使用声明性引用,我们现在可以在优化存储的同时保留映射功能。不过,我们在添加新Book实例时需要小心,因为那些也需要添加到 Publisher的books字段中以建立链接:

template.save(newBook);

template.update(Publisher.class)
    .matching(where("id").is(newBook.publisher.id))
    .apply(new Update().push(
"books", newBook))
    .first();

上面的代码片段很好地概述了处理文档之间链接的非原子性质,这可能需要在Transaction 中运行操作。
  
一对多样式引用
根据应用程序的需要,可以反转Book和Publisher之间的关系,以便将链接元素单独存储在Book文档中。这使您可以存储Book,而无需考虑更新Publisher,正如我们在上一个片段中看到的那样。为此,我们需要做两件事。首先,我们需要告诉映射层省略存储从Publisher到Book的链接,其次,在获取Book引用时更新查找查询。
对books属性应用附加@ReadOnlyPorperty注释;另一部分要求我们使用自定义查询更新@DocumentReference注释的lookup属性:

class Publisher {
    // …
    @ReadOnlyProperty
    @DocumentReference(lookup=
"{'publisher':?#{self._id} }")
    List<Book> books;
}

在上面的代码片段中,我们利用了 Spring Data 查询解析器中的表达式支持。这样做时,我们可以使用self属性访问Publisher原始文档,并可以提取其标识符,然后在查找与其相匹配的Book集合时使用这个标识。
原文见标题