在Spring Data MongoDB中實現關係建模 - spring.io

banq發表於2021-12-09

如何在 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集合時使用這個標識。

原文見標題

相關文章