1. 程式人生 > >MongoDB更需要好的模式設計 及 案例賞析

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的哲學思想。

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