第四章 - 數(shù)據(jù)建模

2018-02-24 16:17 更新

讓我們換換思維,對 MongoDB 進(jìn)行一個更抽象的理解。介紹一些新的術(shù)語和一些新的語法是非常容易的。而要接受一個以新的范式來建模,是相當(dāng)不簡單的。事實(shí)是,當(dāng)用新技術(shù)進(jìn)行建模的時候,我們中的許多人還在找什么可用的什么不可用。在這里我們只是開始新的開端,而最終你需要去在實(shí)戰(zhàn)中練習(xí)和學(xué)習(xí)。

與大多數(shù) NoSQL 數(shù)據(jù)庫相比,面向文檔型數(shù)據(jù)庫和關(guān)系型數(shù)據(jù)庫很相似 - 至少,在建模上是這樣的。但是,不同點(diǎn)非常重要。

No Joins

你需要適應(yīng)的第一個,也是最根本的區(qū)別就是 mongoDB 沒有鏈接(join) 。我不知道 MongoDB 中不支持鏈接的具體原因,但是我知道鏈接基本上意味著不可擴(kuò)展。就是說,一旦你把數(shù)據(jù)水平擴(kuò)展,無論如何你都要放棄在客戶端(應(yīng)用服務(wù)器)使用鏈接。事實(shí)就是,數(shù)據(jù)??關(guān)系, 但 MongoDB 不支持鏈接。

沒別的辦法,為了在無連接的世界生存下去,我們只能在我們的應(yīng)用代碼中自己實(shí)現(xiàn)鏈接。我們需要進(jìn)行二次查詢?find,把相關(guān)數(shù)據(jù)保存到另一個集合中。我們設(shè)置數(shù)據(jù)和在關(guān)系型數(shù)據(jù)中聲明一個外鍵沒什么區(qū)別。先不管我們那美麗的unicorns?了,讓我們來看看我們的?employees。 首先我們來創(chuàng)建一個雇主 (我提供了一個明確的?_id?,這樣我們就可以和例子作成一樣)

db.employees.insert({_id: ObjectId(
    "4d85c7039ab0fd70a117d730"),
    name: 'Leto'})

然后讓我們加幾個工人,把他們的管理者設(shè)置為?Leto:

db.employees.insert({_id: ObjectId(
    "4d85c7039ab0fd70a117d731"),
    name: 'Duncan',
    manager: ObjectId(
    "4d85c7039ab0fd70a117d730")});
db.employees.insert({_id: ObjectId(
    "4d85c7039ab0fd70a117d732"),
    name: 'Moneo',
    manager: ObjectId(
    "4d85c7039ab0fd70a117d730")});

(有必要再重復(fù)一次,?_id?可以是任何形式的唯一值。因?yàn)槟愫芸赡茉趯?shí)際中使用?ObjectId?,我們也在這里用它。)

當(dāng)然,要找出 Leto 的所有工人,只需要執(zhí)行:

db.employees.find({manager: ObjectId(
    "4d85c7039ab0fd70a117d730")})

這沒什么神奇的。在最壞的情況下,大多數(shù)的時間,為彌補(bǔ)無鏈接所做的僅僅是增加一個額外的查詢(可能是被索引的)。

數(shù)組和內(nèi)嵌文檔

MongoDB 不支持鏈接不意味著它沒優(yōu)勢。還記得我們說過 MongoDB 支持?jǐn)?shù)組作為文檔中的基本對象嗎?這在處理多對一(many-to-one)或者多對多(many-to-many)的關(guān)系的時候非常方便。舉個簡單的例子,如果一個工人有兩個管理者,我們只需要像這樣存一下數(shù)組:

db.employees.insert({_id: ObjectId(
    "4d85c7039ab0fd70a117d733"),
    name: 'Siona',
    manager: [ObjectId(
    "4d85c7039ab0fd70a117d730"),
    ObjectId(
    "4d85c7039ab0fd70a117d732")] })

有趣的是,對于某些文檔,manager?可以是單個不同的值,而另外一些可以是數(shù)組。而我們原來的?find?查詢依舊可用:

db.employees.find({manager: ObjectId(
    "4d85c7039ab0fd70a117d730")})

你會很快就發(fā)現(xiàn),數(shù)組中的值比多對多鏈接表(many-to-many join-tables)要容易處理得多。

數(shù)組之外,MongoDB 還支持內(nèi)嵌文檔。來試試看向文檔插入一個內(nèi)嵌文檔,像這樣:

db.employees.insert({_id: ObjectId(
    "4d85c7039ab0fd70a117d734"),
    name: 'Ghanima',
    family: {mother: 'Chani',
        father: 'Paul',
        brother: ObjectId(
    "4d85c7039ab0fd70a117d730")}})

像你猜的那樣,內(nèi)嵌文檔可以用 dot-notation 查詢:

db.employees.find({
    'family.mother': 'Chani'})

我們只簡單的介紹一下內(nèi)嵌文檔適用情況,以及你怎么使用它們。

結(jié)合兩個概念,我們甚至可以內(nèi)嵌文檔數(shù)組:

db.employees.insert({_id: ObjectId(
    "4d85c7039ab0fd70a117d735"),
    name: 'Chani',
    family: [ {relation:'mother',name: 'Chani'},
        {relation:'father',name: 'Paul'},
        {relation:'brother', name: 'Duncan'}]})

反規(guī)范化(Denormalization)

另外一個代替鏈接的方案是對你的數(shù)據(jù)做反規(guī)范化處理(denormalization)。從歷史角度看,反規(guī)范化處理是為了解決那些對性能敏感的問題,或是需要做快照的數(shù)據(jù)(比如說審計日志)。但是,隨著日益增長的普及的 NoSQL,對鏈接的支持的日益喪失,反規(guī)范化作為規(guī)范化建模的一部分變得越來越普遍了。這不意味著,應(yīng)該對你文檔里的每條數(shù)據(jù)都做冗余處理。而是說,與其對冗余數(shù)據(jù)心存恐懼,讓它影響你的設(shè)計決策,不如在建模的時候考慮什么信息應(yīng)當(dāng)屬于什么文檔。

比如說,假設(shè)你要寫一個論壇應(yīng)用。傳統(tǒng)的方式是通過?posts?中的?userid?列,來關(guān)聯(lián)一個特定的?user?和一篇?post。這樣的建模,你沒法在顯示?posts?的時候不查詢 (鏈接到)?users。一個代替案是簡單的在每篇?post?中把?name?和userid?一起保存。你可能要用到內(nèi)嵌文檔,比如?user: {id: ObjectId('Something'), name: 'Leto'}。是的,如果你讓用戶可以更新他們的名字,那么你得對所有的文檔都進(jìn)行更新(一個多重更新)。

適應(yīng)這種方法不是對任何人都那么簡單的。很多情況下這樣做甚至是無意義的。不過不要害怕去嘗試。它只是在某些情況下不適用而已,但在某些情況下是最好的解決方法。

你的選擇是?

在處理一對多(one-to-many)或者多對多(many-to-many)場景的時候,id 數(shù)組通常是一個正確的選擇。但通常,新人開發(fā)者在面對內(nèi)嵌文檔和 "手工" 引用時,左右為難。

首先,你應(yīng)該知道的是,一個獨(dú)立文檔的大小當(dāng)前被限制在 16MB 。知道了文檔的大小限制,挺寬裕的,對你考慮怎么用它多少有些影響。在這點(diǎn)上,看起來大多數(shù)開發(fā)者都愿意手工維護(hù)數(shù)據(jù)引用關(guān)系。內(nèi)嵌文檔經(jīng)常被用到,大多數(shù)情況下多是很小的數(shù)據(jù)塊,那些總是被和父節(jié)點(diǎn)一起拉取的數(shù)據(jù)塊?,F(xiàn)實(shí)的例子是為每個用戶保存一個?addresses?,看起來像這樣:

db.users.insert({name: 'leto',
    email: 'leto@dune.gov',
    addresses: [{street: "229 W. 43rd St",
                city: "New York", state:"NY",zip:"10036"},
               {street: "555 University",
                city: "Palo Alto", state:"CA",zip:"94107"}]})

這并不意味著你要低估內(nèi)嵌文檔的能力,或者僅僅把他們當(dāng)成小技巧。把你的數(shù)據(jù)模型直接映射到你的對象,這會使得問題更簡單,并且通常也不需要用到鏈接了。尤其是,當(dāng)你考慮到 MongoDB 允許你對內(nèi)嵌文檔和數(shù)組的字段進(jìn)行查詢和索引時,效果特別明顯。

大而全還是小而專的集合?

由于對集合沒做任何的強(qiáng)制要求,完全可以在系統(tǒng)中用一個混合了各種文檔的集合,但這絕對是個非常爛的主意。大多數(shù) MongoDB 系統(tǒng)都采用了和關(guān)系型數(shù)據(jù)庫類似的結(jié)構(gòu),分成幾個集合。換而言之,如果在關(guān)系型數(shù)據(jù)庫中是一個表,那么在 MongoDB 中會被作成一個集合 (many-to-many join tables being an important exception as well as tables that exist only to enable one to many relationships with simple entities)。

當(dāng)你把內(nèi)嵌文檔考慮進(jìn)來的時候,這個話題會變的更有趣。常見的例子就是博客。你是應(yīng)該分成一個?posts?集合和一個comments?集合呢,還是應(yīng)該每個?post?下面嵌入一個?comments?數(shù)組? 先不考慮那個 16MB 文檔大小限制 (?_哈姆雷特_全文也沒超過 200KB,所以你的博客是有多人氣?),許多開發(fā)者都喜歡把東西劃分開來。這樣更簡潔更明確,給你更好的性能。MongoDB 的靈活架構(gòu)允許你把這兩種方式結(jié)合起來,你可以把評論放在獨(dú)立的集合中,同時在博客帖子下嵌入一小部分評論 (比如說最新評論) ,以便和帖子一同顯示。這遵守以下的規(guī)則,就是你到想在一次查詢中獲取到什么內(nèi)容。

這沒有硬性規(guī)定(好吧,除了16MB限制)。嘗試用不同的方法解決問題,你會知道什么能用什么不能用。

小結(jié)

本章目標(biāo)是提供一些對你在 MongoDB 中數(shù)據(jù)建模有幫助的指導(dǎo), 一個新起點(diǎn),如果愿意你可以這樣認(rèn)為。在一個面向文檔系統(tǒng)中建模,和在面向關(guān)系世界中建模,是不一樣的,但也沒多少不同。你能得到更多的靈活性并且只有一個約束,而對于新系統(tǒng),一切都很完美。你唯一會做錯的就是你不去嘗試。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號