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的哲学思想。
总之,这是一个多看、多想、多思的蜕变羽化过程,可能时间很长、过程有些痛苦。
本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!
本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!
MongoDB更需要好的模式设计 及 案例赏析的更多相关文章
- MongoDB 进阶模式设计
原文链接:http://www.mongoing.com/mongodb-advanced-pattern-design 12月12日上午,TJ在开源中国的年终盛典会上分享了文档模型设计的进阶技巧,就 ...
- 关系型数据库与Key-value型数据库Mongodb模式设计对比
MongoDb 相比于传统的 SQL 关系型数据库,最大的不同在于它们的模式设计( Schema Design )上的差别,正是由于这一层次的差别衍生出其它各方面的不同. 我们可以简单的认为关系型数据 ...
- MongoDB十二种最有效的模式设计【转】
持续关注MongoDB博客(https://www.mongodb.com/blog)的同学一定会留意到,技术大牛Daniel Coupal 和 Ken W. Alger ,从 今年 2月17 号开始 ...
- php模式设计之 观察者模式
这是我写的<php模式设计>的第五篇.前面的四篇在不断学习不断加深认识,到了今天再看观察者模式,觉得非常容易理解.这也许就是我们积少成多的结果吧.希望还是能够不断进步. 开篇还是从名字说起 ...
- php模式设计之 适配器模式
在这个有没有对象都要高呼“面向对象”的年代,掌握面向对象会给我们带来意想不到的方便.学编程的小伙伴从开始能写几行代码实现简单功能到后来懂得将一些重复的操作组合起来形成一个“函数”,再到后来将“函数”和 ...
- php模式设计之 注册树模式
在前两篇单例模式和工厂模式后,终于迎来了最后一个基础的设计模式--注册树模式. 什么是注册树模式? 注册树模式当然也叫注册模式,注册器模式.之所以我在这里矫情一下它的名称,是因为我感觉注册树这个名称更 ...
- php模式设计之 工厂模式
承接上篇php模式设计之 单例模式,(虽然好像关系不大).今天讲述第二种基础的模式设计——工厂模式. 那么何为工厂模式? 从名字来看,似乎看不出什么端倪.工厂模式,和生产有关?还是和生产流程有关?难道 ...
- php模式设计之 单例模式
模式设计是什么?初学者一开始会被这高大上的名称给唬住.而对于有丰富编程经验的老鸟来说,模式设计又是无处不在.很多接触的框架就是基于各种模式设计形成的. 简单说,在写代码的过程中一开始往往接触的是面向过 ...
- JavaScript高级---门面模式设计
门面模式 两个作用: 1.简化类的接口 2.消除类与使用它的客户代码之间的耦合 门面模式常常是开发人员最亲密的朋友.它几乎是所有javascript库的核心原则 门面模式的目的是为了让开发人员用更简单 ...
随机推荐
- [Swift]LeetCode698. 划分为k个相等的子集 | Partition to K Equal Sum Subsets
Given an array of integers nums and a positive integer k, find whether it's possible to divide this ...
- [Swift]LeetCode869. 重新排序得到 2 的幂 | Reordered Power of 2
Starting with a positive integer N, we reorder the digits in any order (including the original order ...
- Linux中断程序命令
在运行 python 脚本的时候想要中断程序,发现如下情况: ctrl+c 居然无法中断程序! 这时候尝试 ctrl+d 还是毫无效果,最后尝试 ctrl+\: 查看该程序是否还在运行 ps aux ...
- 【JMeter】(3)---MySQL压测
JMeter---MySQL压测 一.添加MySQL驱动jar包 在测试计划(Test Plan)页面添加jar包 二.添加MySQL连接配置 添加线程组右击-->添加(add)-->配置 ...
- windows部署MongoDB
打开MongoDb下载页面,分别下载Community Server和Compass,注意在安装Community Server时可以勾选同时安装Compass,但会比较慢,所以建议两个分开下载安装. ...
- Python爬虫入门教程 3-100 美空网数据爬取
美空网数据----简介 从今天开始,我们尝试用2篇博客的内容量,搞定一个网站叫做"美空网"网址为:http://www.moko.cc/, 这个网站我分析了一下,我们要爬取的图片在 ...
- Android创建或删除了文件,在电脑端查看的时候,却没有对应的变化,处理办法
在Android应用中,碰到一个问题,在代码中执行创建或者删除某个文件后,在系统的文件管理器中能够相应地看到文件的变化,但是插在电脑上查看的时候,却看不到改文件的变化.同时,当创建文件后,在系统中的某 ...
- Solr 12 - 部署SolrCloud中遇到的问题 + 解决方法
目录 1 ZooKeeper管理配置文件的另一种方法 2 Solr服务不能访问 3 部分节点处于"Recovering"或"Gone"状态 4 Solr集群不稳 ...
- 初探Java设计模式3:行为型模式(策略,观察者等)
行为型模式 行为型模式关注的是各个类之间的相互作用,将职责划分清楚,使得我们的代码更加地清晰. 策略模式 策略模式太常用了,所以把它放到最前面进行介绍.它比较简单,我就不废话,直接用代码说事吧. 下面 ...
- JVM(1)---虚拟机在运行期的优化策略
1.解释器与JIT编译器 首先我们先来了解一下运行在虚拟机之上的解释器与JIT编译器.当我们的虚拟机在运行一个java程序的时候,它可以采用两种方式来运行这个java程序: 采用解释器的形式,也就是说 ...