MongoDB实战-面向文档的数据(找到最合适的数据建模方式)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MongoDB实战-面向文档的数据(找到最合适的数据建模方式)相关的知识,希望对你有一定的参考价值。

  前一段时间一直研究通过Ruby操作MongoDB数据库,在学习的过程中也分享了自己学习成长的过程,撰写了包含两篇入门操作文章和十二篇进阶文章。本篇文章开始,我们将进入MongoDB的实战操作流程,MongoDB这一非关系型数据库-是一个文档型数据库,存储的是面向文档的数据。

  1. 如何在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实战-面向文档的数据(找到最合适的数据建模方式)的主要内容,如果未能解决你的问题,请参考以下文章

干货满满 | MongoDB集群实战攻略

存储数千个中型文档的最有效的面向文档的数据库引擎是啥?

MongoDB入门实战教程

MongoDB学习笔记

Nosql中MongoDB的介绍

寻找从 mongodb 到 tableau 的中间件