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


一  挑戰

設計從來就是個挑戰。

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

很多時候,我們錯誤地認為,花費大量時間用在設計上,問題根源在於關系數據庫(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的哲學思想。

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

 

本文版權歸作者所有,未經作者同意不得轉載,謝謝配合!!!

本文版權歸作者所有,未經作者同意不得轉載,謝謝配合!!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM