一 挑戰 設計從來就是個挑戰。 當我們第一次接觸資料庫,學習資料庫基礎理論時,都需要學習範式,老師也一再強調範式是設計的基礎。範式是這門課程中的重要部分,在期末考試中也一定是個重要考點。如果我們當年大學掛科了,說不定就是範式這道題沒有做好。畢業後,當我們面試時,往往也有關於表設計方面拷問。 很多時候 ...
一 挑戰
設計從來就是個挑戰。
當我們第一次接觸資料庫,學習資料庫基礎理論時,都需要學習範式,老師也一再強調範式是設計的基礎。範式是這門課程中的重要部分,在期末考試中也一定是個重要考點。如果我們當年大學掛科了,說不定就是範式這道題沒有做好。畢業後,當我們面試時,往往也有關於表設計方面拷問。
很多時候,我們錯誤地認為,花費大量時間用在設計上,問題根源在於關係資料庫(RDBMS),在於二維表及其之間的聯繫組成的一個數據組織。而真實的環境中,我們正在大量使用noSQL或者NewSQL,按照目前的趨勢(DB-Engines Ranking 得分),將來還會越來越普遍。選用noSQL或者NewSQL 就不需要模式設計了。並且,隨著公司、行業數字化程度的加深,智能化觸角逐漸延伸,數據量越來越大,結構越來越複雜。 例如現在很火的IOT行業,複雜的業務信息、多樣的傳輸協議、不斷升級的感測器,都需要靈活的數據模型來應對。在這種呼喚聲中,MongoDB閃亮登場了。MongoDB支持靈活的數據模型。主要體現在以下2點:
(1)自由模式,無需提前聲明、創建表結構,即不用先創建表、添加欄位,然後才可以Insert數據。預設情況下MongoDB無需這樣操作,除非開啟了模式驗證。
(2)鍵值類型自由,MongoDB 將數據存儲為一個文檔,數據結構由鍵值(key=>value)對組成。欄位值可以包含其他文檔,數組及文檔數組。
MongoDB不需要模式設計時錯誤的,其實面對複雜的結構對象,模式的自由帶來更大的挑戰。
模式的自由是對數據insert這個動作而言,它去除很多限制了,可以快速講對象的存進來,並且易於擴展。但是不一定就會帶來好的查詢性能,好的查詢性能還要來自於好的模式設計、來自於好的集合文檔的設計。
二 模式設計
MongoDB可以將模式設計劃分為內嵌模式(Embedded)和 引用模式(References)
內嵌模式
簡單來講,內嵌模式就是將關聯數據,放在一個文檔中。例如以下員工信息採用內嵌模式了而存儲在了一個文檔中:
引用模式
引用模式是將數據存儲在不同集合的文檔中,而通過關係數據進行關聯。例如,這裡採用引用模式將員工信息存儲在了3個文檔中,基本信息一個文檔,聯繫方式一個文檔,登錄許可權放在了一個文檔中。每個文檔之前通過user_id來關聯。
三 案例
下麵我們通過一些業務場景,一些具體的案例,來分析、品味一下MongoDB模式設計的選擇。
案例 1
假如現在我們描述來顧客(patron)和顧客的地址(address),其ER圖如下:
我們可以將patron和address設計成兩個集合(collection,類似於RDBMS資料庫中的table),其具體信息如下:
patron 集合
{
_id: "joe",
name: "Joe Bookreader"
}
address 集合
{
patron_id: "joe",
street: "123 Fake Street",
city: "Faketon",
state: "MA",
zip: "12345"
}
在設計address 集合時,內嵌了patron集合的_id欄位,通過這個欄位進行關聯。
但這種實體關係為1:1,強關聯的關係
推薦設計成如下模式:
{
_id: "joe",
name: "Joe Bookreader",
address: {
street: "123 Fake Street",
city: "Faketon",
state: "MA",
zip: "12345"
}
}
即使用內嵌模式,將數據存儲在一個集合中。
案例2
一個顧客維護一個地址是理想的狀況,回頭看看我們淘寶賬號,就會發現收貨地址一般都是2個以上 ( 流淚 ╥╯^╰╥)
patron 集合顧客joe的文檔記錄
{
_id: "joe",
name: "Joe Bookreader"
}
address 集合joe顧客的地址1的文檔記錄
{
patron_id: "joe",
street: "123 Fake Street",
city: "Faketon",
state: "MA",
zip: "12345"
}
address 集合中joe顧客的地址2的文檔記錄
{
patron_id: "joe",
street: "1 Some Other Street",
city: "Boston",
state: "MA",
zip: "12345"
}
像這種1:N的關係,並且N可以預見不是很多的情況下,我們推薦採用內嵌模式,
將集合文檔設計成如下模式:
{
_id: "joe",
name: "Joe Bookreader",
addresses: [
{
street: "123 Fake Street",
city: "Faketon",
state: "MA",
zip: "12345"
},
{
street: "1 Some Other Street",
city: "Boston",
state: "MA",
zip: "12345"
}
]
}
與案例1的不同就是地址信息採用了數組類型,數組的欄位值又為內嵌子文檔。
案例3
上面介紹的是1對多的關係(1:N),但是N值不是很大。但是現實世界中,有時候會遇到N值比較大的情況。
比如 出版社和書籍的關係,一個出版社可能已將出版了成千上萬本書籍了。
其設計模式可以如下(內嵌模式),將出版社的信息作為一個子文檔,來內嵌到書籍的文檔中,具體信息如下:
以下書籍《MongoDB: The Definitive Guide》的文檔信息:
{
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English",
publisher: {
name: "O'Reilly Media",
founded: 1980,
location: "CA"
}
}
以下書籍《50 Tips and Tricks for MongoDB Developer》的文檔信息:
{
title: "50 Tips and Tricks for MongoDB Developer",
author: "Kristina Chodorow",
published_date: ISODate("2011-05-06"),
pages: 68,
language: "English",
publisher: {
name: "O'Reilly Media",
founded: 1980,
location: "CA"
}
}
從中可以看出,publisher信息描述比較多,並且都相同,每個文檔中都存放,浪費太多的存儲空間,顯得無用臃腫,還有個明顯的缺點就是 當publisher數據更新時,需要對所有的書籍文檔進行刷新。理所當然地,就會想到將出版社獨立出來,單獨設計一個文檔。(引用模式)。
引用模式1
我們可以這樣設計:出版社單獨設計為一個集合文檔(文檔中引用書籍的編號),如下:
{
name: "O'Reilly Media",
founded: 1980,
location: "CA",
books: [123456789, 234567890, ...]
}
書籍集合中編號為123456789的書籍的文檔:
{
_id: 123456789,
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English"
}
書籍集合中編號為234567890的書籍的文檔:
{
_id: 234567890,
title: "50 Tips and Tricks for MongoDB Developer",
author: "Kristina Chodorow",
published_date: ISODate("2011-05-06"),
pages: 68,
language: "English"
}
此設計中,將出版社出版的書的編號,保存在了出版社這個集合中。
但是這種設計還是有問題,例如,數組的更新、刪除相對比較困難。還有就是,每增加一個書籍集合的文檔,同時還要修改這個出版社結合的文檔。 所以,我們還可以將這種集合文檔設計優化如下。
引用模式2
此時出版社的文檔記錄如下:(不再應用書籍文檔的編號)
{
_id: "oreilly",
name: "O'Reilly Media",
founded: 1980,
location: "CA"
}
此時書籍的文檔記錄如下:(書籍為123456789,文檔引用了出版社的_ID)
{
_id: 123456789,
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English",
publisher_id: "oreilly"
}
此時書籍的文檔記錄如下:(書籍為234567890,文檔引用了出版社的_ID)
{
_id: 234567890,
title: "50 Tips and Tricks for MongoDB Developer",
author: "Kristina Chodorow",
published_date: ISODate("2011-05-06"),
pages: 68,
language: "English",
publisher_id: "oreilly"
}
案例 4
上面三個例子,在關係型資料庫中都可以用我們學習過的關係(例如1:1;1:N)來描述,那麼我們再舉一個關係型資料庫難以描述的關係 -- 樹狀關係。
例如,我們在電商網站上常見的商品分類關係,一級商品、二級商品、三級商品、四級商品關係。我們簡化此例子如下:
那麼在MongoDB中可以輕鬆實現他們關係的查詢。
情景1 查詢節點的父節點(或稱為查詢上一級分類);或者查詢節點的子節點(或者為查詢下一級分類)
文檔的設計為:
db.categories.insert( { _id: "MongoDB", parent: "Databases" } )
db.categories.insert( { _id: "dbm", parent: "Databases" } )
db.categories.insert( { _id: "Databases", parent: "Programming" } )
db.categories.insert( { _id: "Languages", parent: "Programming" } )
db.categories.insert( { _id: "Programming", parent: "Books" } )
db.categories.insert( { _id: "Books", parent: null } )
查詢節點的父節點(或稱為查詢上一級分類)的語句,例如查詢MongoDB所屬分類:
db.categories.findOne( { _id: "MongoDB" } ).parent
查詢節點的子節點(或者為查詢下一級分類),例如查詢Database的直連的子節點(不是孫子節點)。
db.categories.find( { parent: "Databases" } )
上面的文檔可以查詢出子文檔,但是會顯示出多個文檔,例如上面的查詢語句,會返回出MongoDB 文檔和 dbm文檔 ,我們還需要還特殊處理,那麼可不可以在一個文檔中顯示出所以的子節點呢?
可以的。文檔模式設計如下:
db.categories.insert( { _id: "MongoDB", children: [] } )
db.categories.insert( { _id: "dbm", children: [] } )
db.categories.insert( { _id: "Databases", children: [ "MongoDB", "dbm" ] } )
db.categories.insert( { _id: "Languages", children: [] } )
db.categories.insert( { _id: "Programming", children: [ "Databases", "Languages" ] } )
db.categories.insert( { _id: "Books", children: [ "Programming" ] } )
如果這時候查詢Databases的子節點,就會是一個文檔了。查詢驗證語句如下:
db.categories.findOne( { _id: "Databases" } ).children
此模式也支持查詢節點的父節點。例如查詢MongoDB這個節點的父節點:
db.categories.find( { children: "MongoDB" } )
情景2 查詢祖先節點
其文檔設計為:
db.categories.insert( { _id: "MongoDB", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" } )
db.categories.insert( { _id: "dbm", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" } )
db.categories.insert( { _id: "Databases", ancestors: [ "Books", "Programming" ], parent: "Programming" } )
db.categories.insert( { _id: "Languages", ancestors: [ "Books", "Programming" ], parent: "Programming" } )
db.categories.insert( { _id: "Programming", ancestors: [ "Books" ], parent: "Books" } )
db.categories.insert( { _id: "Books", ancestors: [ ], parent: null } )
例如查詢MongoDB節點的祖先節點:
db.categories.findOne( { _id: "MongoDB" } ).ancestors
當然也可以查詢 後代節點:
db.categories.find( { ancestors: "Programming" } )
四 後記
MongoDB的模式設計是一個比較大的課題,需要多看看情景案例,多品味一些優秀的文檔設計,多問些問什麼要這樣做,是否有更優的設計,要慢慢去領悟MongoDB的哲學思想。
總之,這是一個多看、多想、多思的蛻變羽化過程,可能時間很長、過程有些痛苦。
本文版權歸作者所有,未經作者同意不得轉載,謝謝配合!!!
本文版權歸作者所有,未經作者同意不得轉載,謝謝配合!!!