MongoDB更須要好的模式設計 及 案例賞析

一  挑戰

設計歷來就是個挑戰。面試

當咱們第一次接觸數據庫,學習數據庫基礎理論時,都須要學習範式,老師也一再強調範式是設計的基礎。範式是這門課程中的重要部分,在期末考試中也必定是個重要考點。若是咱們當年大學掛科了,說不定就是範式這道題沒有作好。畢業後,當咱們面試時,每每也有關於表設計方面拷問。數據庫

不少時候,咱們錯誤地認爲,花費大量時間用在設計上,問題根源在於關係數據庫(RDBMS),在於二維表及其之間的聯繫組成的一個數據組織。而真實的環境中,咱們正在大量使用noSQL或者NewSQL,按照目前的趨勢(DB-Engines Ranking 得分),未來還會愈來愈廣泛。選用noSQL或者NewSQL 就不須要模式設計了。而且,隨着公司、行業數字化程度的加深,智能化觸角逐漸延伸,數據量愈來愈大,結構愈來愈複雜。 例如如今很火的IOT行業,複雜的業務信息、多樣的傳輸協議、不斷升級的傳感器,都須要靈活的數據模型來應對。在這種呼喚聲中,MongoDB閃亮登場了。MongoDB支持靈活的數據模型。主要體如今如下2點:設計模式

(1)自由模式,無需提早聲明、建立表結構,即不用先建立表、添加字段,而後才能夠Insert數據。默認狀況下MongoDB無需這樣操做,除非開啓了模式驗證。數組

(2)鍵值類型自由,MongoDB 將數據存儲爲一個文檔,數據結構由鍵值(key=>value)對組成。字段值能夠包含其餘文檔,數組及文檔數組。數據結構

MongoDB不須要模式設計時錯誤的,其實面對複雜的結構對象,模式的自由帶來更大的挑戰。ide

模式的自由是對數據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的哲學思想。

總之,這是一個多看、多想、多思的蛻變羽化過程,可能時間很長、過程有些痛苦。

 

本文版權歸做者全部,未經做者贊成不得轉載,謝謝配合!!!

本文版權歸做者全部,未經做者贊成不得轉載,謝謝配合!!!

相關文章
相關標籤/搜索