標籤:schema mongodb 電子商務
前一段時間一直研究通過Ruby操作MongoDB資料庫,在學習的過程中也分享了自己學習成長的過程,撰寫了包含兩篇入門操作文章和十二篇進階文章。本篇文章開始,我們將進入MongoDB的實戰操作流程,MongoDB這一非關係型資料庫-是一個文檔型資料庫,儲存的是面向文檔的資料。
如何在MongoDB資料庫中使用schema
設計資料庫schema是在已知資料庫系統特性、資料本質以及應用程式需求的情況下為資料集選擇最佳表述的過程。傳統的關係型資料庫RDBMS中鼓勵使用正規化的資料模型,從而確保資料的可查詢性和解決資料更新帶來的不一致問題。但是schema的設計不是一門精確的科學。當出現要應用程式處理非結構化資料,或者應用程式對效能要求很高時,就可能會要求一個通用的資料模型。MongoDB中缺乏硬性Schema設計規則。
為了能夠參考傳統RDBMS的schema設計規則,我們首先需要清楚RDBMS和MongoDB在如下三個方面的對應關係和相應區別:
資料的基本單元分別是什嗎?
在RDBMS中,資料的基本單元指的是帶有列和行的資料表;
在KVStore for Redis中指向不定類型值的鍵;
在MongoDB中,資料的基本類型是BSON文檔
如何查詢和更新資料?
資料查詢操作中:
RDBMS支援即時查詢和連接操作查詢;
MongoDB支援即時查詢,但是不支援連接操作;
簡單的KVStore for Redis只能根據單個鍵來擷取值
資料更新操作中:
RDBMS中,可以使用SQL以複雜的方式來更新文檔,將多條更新封裝在一個事務中可以獲得原子性,還可以復原;
MongoDB不支援事務,但支援多種原子操作,這些操作可以作用於複雜文檔的內部結構;
簡單的KVStore for Redis中,可以更新一個值,通常每次更新都是將值完全替換掉。
應用程式的訪問模式是什嗎?
要想確定理想的資料模型,必須問無數個與應用程式有關的問題。讀寫比?需要何種查詢?資料如何更新?並發問題?資料機構化程度?
總的來說,最好的schema設計總是源於對正在使用的資料庫的深入理解,對應用程式需求的準確判斷以及過去的經驗。
2. 實戰-設計電子商務資料模型
在本部分,我們將示範如何在MongoDB中對電子商務資料進行建模,我們會關注產品與分類、使用者與訂單、產品評論。對很多開發人員來講,資料建模總會伴隨著對象映射。使用對象映射器有利於進行驗證、類型檢查和關聯。MongoDB沒有對象映射的需要,一方面是因為文檔已經是類似對象的表述了,同時驅動程式為MongoDB提供了相當高階的介面,參考前序博文的學習,使用驅動介面就能在MongoDB上構建完整的應用程式。很多成熟的MongoDB對象映射器在基礎語言驅動之上又提供了一層額外的抽象。
由於最終還是需要跟文檔打交道,關注文檔本身,認識到一個精心設計的MongoDB Schema裡文檔是什麼樣的,能讓我們更好的使用該資料庫。
2.1 產品與分類
產品和分類是會出現在任何電子商務網站的資訊。在傳統的RDBMS中,產品會使用大量的資料表,比如儲存基本資料的表,儲存關聯送貨資訊和價格曆史的表,以及其他可能會出現的一系列複雜屬性。這種多表schema在RDBMS的表連接能力的協助下很有用。
但是在MongoDB資料庫中,對產品進行建模會相對簡單。集合并不一定有schema。任何產品資訊文檔都可以容納產品所需的各種動態屬性。通過使用數組來容納內部文檔結構,還可以將RDBMS裡的多表描述為一個MongoDB集合。
下面是一個取自園藝商店的樣本產品
doc={ _id:new ObjectId("59884b76b53fab2a8024b6ad"), slug:"wheel-barrow-9092", sku:"9092", name:"Extra Large Wheel Barrow", description:"Heavy duty wheel barrow", details:{ weight:47, weight_unite:"1bs", model_num:40392882, manufacturer:"Acme", color:"Green" }, total_review:4, average_review:4.5, pricing: { retail:589700, sale:489700 }, price_history:[ { retail:529700, sale:429700, start:new Date(2010,4,1), end:new Date(2010,4,8) }, { retail:529700, sale:529700, start:new Date(2010,4,9), end:new Date(2010,4,16) } ], cateory_ids:[ new ObjectId("59884ee3b53fab2a8024b6ae"), new ObjectId("59884ee3b53fab2a8024b6af") ], main_cate_id:new ObjectId("59884ee3b53fab2a8024b6b1"), tags:["tools","gardening","soil"]}
在此,如果要為文檔產生一個URL,通常建議設定一個簡短名稱欄位。且該欄位應該有唯一索引,這樣就可以把其中的值用作主鍵。假設將此文檔儲存在products集合裡,可以像下面一樣建立唯一性索引。
db.products.ensureIndex({slug:1},{unique:true})
由於在slug上存在唯一索引,插入文檔時需要使用安全模式,這樣就可以知道插入成功與否。在Ruby中執行插入代碼
@products.insert({:name=>"Extra Large Wheel Barrow", :sku=>"9092", :slug=>"wheel-barrow-9092"}, :safe=>true )
代碼中的:safe=>true;如果插入成功,就不會拋出異常,表明選擇了一個唯一的簡短名稱;如果拋出異常,代碼需要使用一個新的簡短名稱進行重試。上述文檔中後續儲存了details-不同產品的詳細資料,接著儲存了當前價格pricing和曆史價格price_history,category_ids儲存了標籤名稱的數組。
RDBMS資料庫可以使用join操作進行多表聯集查詢。作為不支援連接查詢的MongoDB資料庫,如何支援多對多的策略呢?文檔中儲存category_ids數組,其中包含的是一個對象ID的數組,每個對象ID都是一個指標,指向某個分類文檔的_id欄位。下面是一個分類文檔的示範:
doc={ _id:new ObjectId("59884ee3b53fab2a8024b6ae"), slug:"gradening-tools", ancestors:[ { name:"Home", _id:new ObjectId("59884ee3b53fab2a80240003"), slug:"home" }, { name:"Outdoors", _id:new ObjectId("59884ee3b53fab2a80240001"), slug:"outdoors" } ], parent_id:new ObjectId("59884ee3b53fab2a80240001"), name:"Gardening Tools", description:"Gardening gadgets galore"}
觀察產品文檔的category_ids欄位裡的對象ID,發現該產品關聯了Gardening Tools分類。在產品文檔中放入category_ids的數組鍵讓那些多對多的查詢成為可能。
查詢Gardening Tools分類裡的所有產品
db.products.find({category_ids=>category{‘_id‘}})
查詢指定產品的所有分類,可以使用$in操作符,它類似於SQL的IN指令。
db.categories.find({_id:{$in:procuct[‘category_ids‘]}})
分類文檔中,存放父文檔數組的含義是去正規化,將上級分類的名稱放入每個子分類的文檔裡,這也是由於MongoDB不支援關聯查詢。這樣一來,查詢Gardening Tools分類時,就不需要執行額外的查詢來擷取上級分類(Outdoors和Home)的名稱和URL了。
2.2 使用者與訂單
看看如何對使用者和訂單建模,以此闡明另一種常見關係——一對多關聯性。一個使用者可能會擁有多張訂單。在RDBMS中,會在訂單表中使用外鍵;在MongoDB中慣例很相似,如:
doc={ _id:new ObjectId("6a5b1476238d3b4dd5000001"), user_id:new ObjectId("4a5b1476238d3b4dd5000001"), state:"CART", line_items:[{ _id:new ObjectId("4a5b1472134d3b4dd5000921"), sku:"9092", name:"Extra Large Wheel Barrow", quantity:1, pricing:{ retail:5897, sale:4897, } }, { _id:new ObjectId("4a5b1472134d3b4dd5000922"), sku:"10027", name:"Rubberized Work Glove,Block", quantity:2, pricing:{ retail:1499, sale:1299, } } ], shipping_address:{ street:"588 5th Street", city:"Brooklyn", state:"NY", zip:11215 }, sub_total:6196}
訂單中的第二個屬性user_id儲存了一個使用者的_id,它是指指向樣本使用者的指標。這樣的設計可以方便地查詢關係中的任意一方。要尋找一個使用者的所有訂單:
db.orders.find({user_id:user{‘_id‘}})
要擷取指定訂單的使用者同樣簡單:
user_id=order[‘user_id‘]db.users.find({_id:user_id})
上面的訂單表述方式有明顯的優點,首先,它易於理解,完整的訂單概念都能被封裝在一個實體裡,包括條目明細、寄送地址以及最終的支付資訊。查詢資料庫時,可以通過一條簡單的查詢返回整個訂單對象。其次,可以把產品在購買時的資訊儲存在訂單文檔裡,這樣能夠輕易地查詢並修改訂單資訊。
使用者的文檔也使用了類似的模式。儲存了一個地址文檔的列表和一個支付方式的列表。在文檔的最上層還能找到任何使用者模型裡都有的基本常見屬性。
doc={_id:new ObjectId("4a5b1476238d3b4dd5000001"),email:"[email protected]",first_name:"Kyle",last_name:"Banker",hashed_password:"bd1cfa194c3a603e7186780824b04419",address:[{ name:"home", street:"588 5th Street", city:"Brooklyn", state:"NY", zip:10010},{ name:"work", street:"1 E.23rd Street", city:"New York", state:"NY", zip:10010 }],payment_methods:[{ name:"VISA", last_four:2127, crypted:"43f6baldfda6b8106dc7", expiration_date:new Date(2014,4)}]}
2.3 評論資訊
一般產品都會有評論資訊。一般而言,一個產品有多個評論,該關係是也用對象ID應用product_id來編碼的。
doc={ _id:new ObjectId("4c4b1476238d3b4dd5000041"), product_id:new ObjectId("59884b76b53fab2a8024b6ad"), date:new Date(2010,5,7), title:"Amazing", text:"Has a squeaky wheel,but still a darn good wheel barrow", rating:4, user_id:new ObjectId("4a5b1476238d3b4dd5000001"), user_name:"dgreenthumb", helpful_votes:3, voter_ids:[ {new ObjectId("59884b76b53fab2a8024b600")}, {new ObjectId("59884b76b53fab2a8024b601")}, {new ObjectId("59884b76b53fab2a8024b602")}}
上面的評估資訊中,由於MongoDB不支援連接查詢,所以冗餘儲存了user_name,同時還有一個voter_ids數組,用於儲存對該評論進行投票的使用者。去除了重複投票,同時也讓我們有能力查詢某個使用者投過票的所有評論。
至此,我們已經覆蓋了電子商務的資料模型了,講解了具體的建模方法,以及由於MongoDB不支援連接查詢帶來的局限性問題的去正規化解決方案,從而找到一個最適用於應用的schema。
本文出自 “techFuture” 部落格,謝絕轉載!
MongoDB實戰-面向文檔的資料(找到最合適的資料建模方式)