MongoDB实战-面向文档的数据(找到最合适的数据建模方式)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MongoDB实战-面向文档的数据(找到最合适的数据建模方式)相关的知识,希望对你有一定的参考价值。
前一段时间一直研究通过Ruby操作MongoDB数据库,在学习的过程中也分享了自己学习成长的过程,撰写了包含两篇入门操作文章和十二篇进阶文章。本篇文章开始,我们将进入MongoDB的实战操作流程,MongoDB这一非关系型数据库-是一个文档型数据库,存储的是面向文档的数据。
如何在MongoDB数据库中使用schema
设计数据库schema是在已知数据库系统特性、数据本质以及应用程序需求的情况下为数据集选择最佳表述的过程。传统的关系型数据库RDBMS中鼓励使用正规化的数据模型,从而确保数据的可查询性和解决数据更新带来的不一致问题。但是schema的设计不是一门精确的科学。当出现要应用程序处理非结构化数据,或者应用程序对性能要求很高时,就可能会要求一个通用的数据模型。MongoDB中缺乏硬性Schema设计规则。
为了能够参考传统RDBMS的schema设计规则,我们首先需要清楚RDBMS和MongoDB在如下三个方面的对应关系和相应区别:
数据的基本单元分别是什么?
在RDBMS中,数据的基本单元指的是带有列和行的数据表;
在键值存储中指向不定类型值的键;
在MongoDB中,数据的基本类型是BSON文档
如何查询和更新数据?
数据查询操作中:
RDBMS支持即时查询和联结操作查询;
MongoDB支持即时查询,但是不支持联结操作;
简单的键值存储只能根据单个键来获取值
数据更新操作中:
RDBMS中,可以使用SQL以复杂的方式来更新文档,将多条更新封装在一个事务中可以获得原子性,还可以回滚;
MongoDB不支持事务,但支持多种原子操作,这些操作可以作用于复杂文档的内部结构;
简单的键值存储中,可以更新一个值,通常每次更新都是将值完全替换掉。
应用程序的访问模式是什么?
要想确定理想的数据模型,必须问无数个与应用程序有关的问题。读写比?需要何种查询?数据如何更新?并发问题?数据机构化程度?
总的来说,最好的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实战-面向文档的数据(找到最合适的数据建模方式)的主要内容,如果未能解决你的问题,请参考以下文章