NoSQL | MongoDB入門實(shí)戰(zhàn)教程(7)

前面我們學(xué)習(xí)了聚合查詢,本篇我們來看看在模型設(shè)計(jì)中如何應(yīng)用引用模式來提高查詢效率。
在進(jìn)行MongoDB的模型設(shè)計(jì)中,基于JSON文檔模型,我們很容易就可以設(shè)計(jì)出一個(gè)內(nèi)嵌模式的文檔模型出來。
可以不夸張地說,80%~90%的場景下,我們優(yōu)先都會(huì)使用內(nèi)嵌對(duì)象 或 內(nèi)嵌數(shù)組 的方式來設(shè)計(jì)文檔模型的所謂的1-1、1-N、N-N的關(guān)系。
例如下面這個(gè)Contacts的文檔模型,它描述了一個(gè)聯(lián)系人的關(guān)系建模:
Contacts{name: "Edison Zhou",company: "CSCEC YZW",title: ".NET Engineer",portraits: {mimetype: xxx,data: xxxx},addresses: [{ type: home, … },{ type: work, … }],groups: [{name: "YZW Football Assocation" },{name: "YZW .NET Assocation" }]}
可以看到,所謂的內(nèi)嵌類 其實(shí) 類似于 預(yù)先聚合(關(guān)聯(lián)),這樣的操作(引用+冗余)其實(shí)對(duì)讀操作更有性能優(yōu)勢。
但是,內(nèi)嵌設(shè)計(jì)有一個(gè)大前提限制:即內(nèi)嵌后文檔大小不能超過16MB。
此外,如果內(nèi)嵌的數(shù)組(通常是數(shù)組)的長度太大,比如數(shù)萬或更多的時(shí)候,也是不適合采用內(nèi)嵌模式的。
那么,此時(shí)我們應(yīng)該怎么設(shè)計(jì)呢?
萬級(jí)長度的內(nèi)嵌數(shù)組
這里我們?nèi)匀贿m用上面提到的Contacts模型,假設(shè)其中的groups是一個(gè)內(nèi)嵌數(shù)組,這個(gè)groups的數(shù)據(jù)可能有百萬級(jí)的長度,且每個(gè)Contacts文檔都需要冗余這么一份數(shù)據(jù),而且groups數(shù)據(jù)還面臨著頻繁修改的需求。
Contacts{name: "Edison Zhou",company: "CSCEC YZW",title: ".NET Engineer",......// 假設(shè)下面groups有百萬級(jí),且一個(gè)group的信息改動(dòng)會(huì)引發(fā)百萬級(jí)的DB操作groups: [: "YZW Football Assocation" },: "YZW .NET Assocation" }]}
適當(dāng)使用引用模式解決
解決方案很簡單,就是針對(duì)groups使用單獨(dú)的collection來存儲(chǔ),在Contancts模型中添加對(duì)group id的集合的引用。
Collection 1 - Contacts:
Contacts{name: "Edison Zhou",company: "CSCEC YZW",title: ".NET Engineer",......// 假設(shè)下面groups有百萬級(jí),且一個(gè)group的信息改動(dòng)會(huì)引發(fā)百萬級(jí)的DB操作group_ids: [1,2,3,4,5...]}
Collection 2 - Groups:
Groups{groups_id,name}
這樣的設(shè)計(jì)其實(shí)類似于關(guān)系型數(shù)據(jù)庫模型的設(shè)計(jì),用Id來關(guān)聯(lián),我們再熟悉不過了。
但是,在MQL中,我們就需要額外使用$lookup來實(shí)現(xiàn)類似SQL中的關(guān)聯(lián)查詢了,嚴(yán)格來說,應(yīng)該算是LEFT OUTER JOIN查詢。
嗯,這又是一種聚合操作:
db.Contacts.aggregate([{$lookup:{from: "groups",localField: "group_ids",foreignField: "group_id",as: "groups"}}]);
這個(gè)查詢會(huì)得到如下圖所示的結(jié)果:

.NET中的Lookup操作:
上面講解了如何通過MQL進(jìn)行操作,那么,在.NET中如何實(shí)現(xiàn)$lookup的效果呢?
好在MongoDB Driver已經(jīng)幫我們提供了這樣的一個(gè)LookUp,且看下面的代碼示例:
假設(shè)我們的實(shí)體定義如下:
public class Contact{[][]public string Id { get; set; }public string Name { get; set; }public string Company { get; set; }public string Title { get; set; }public int[] GroupIds { get; set; }public IList<Group> Groups { get; set; }}public class Group{[][]public string Id { get; set; }public int GroupId { get; set; }public string Name { get; set; }}
那么,可以通過Driver實(shí)現(xiàn)以下操作:
public async Task<IList<Contact>> GetAsync(){return await _contacts.Aggregate().Lookup<Contact, Group, Contact>(_groups,local => local.GroupIds,from => from.GroupId,result => result.Groups).ToListAsync();}
完整示例github地址:https://github.com/EdisonChou/EDT.Mongo.Sample
運(yùn)行結(jié)果如下所示:

什么時(shí)候使用引用模式
綜上所述,當(dāng)滿足以下條件之一時(shí),你可以開始考慮引用模式設(shè)計(jì)文檔模型:
(1)當(dāng)內(nèi)嵌后的文檔太大,有可能超過16MB限制的時(shí)候;
(2)內(nèi)嵌的文檔 或 數(shù)組元素 有可能會(huì)頻繁修改的時(shí)候;
(3)內(nèi)嵌數(shù)組元素 有可能會(huì)持續(xù)增長且沒有封頂?shù)臅r(shí)候;
引用模式設(shè)計(jì)的限制
引用模式也并非銀彈,它存在以下一些限制:
(1)MongoDB對(duì)于使用引用的集合之間沒有所謂的外鍵檢查;
(2)MongoDB使用聚合框架的$lookup來模仿關(guān)聯(lián)查詢;
(3)$lookup只支持LEFT OUTER JOIN,且關(guān)聯(lián)目標(biāo)(from)不能是分片表;
db.Contacts.aggregate([{$lookup:{from: "groups", // 這里的from不能是分片表......}}]);
本文簡單介紹了MongoDB的模型設(shè)計(jì)中的內(nèi)嵌模式和引用模式,探討了引用模式的使用、何時(shí)使用 及 使用限制。
下一篇,我們會(huì)學(xué)習(xí)MongoDB的模式設(shè)計(jì)中的一些設(shè)計(jì)模式并套用這些設(shè)計(jì)模式簡化設(shè)計(jì)難度。
參考資料
唐建法,《MongoDB高手課》(極客時(shí)間)
郭遠(yuǎn)威,《MongoDB實(shí)戰(zhàn)指南》(圖書)
