MongoDB模式设计及案例

1、无模式

        当我们第一次接触数据库,学习数据库基础理论时,都需要学习范式,老师也一再强调范式是设计的基础。范式是这门课程中的重要部分,在期末考试中也一定是个重要考点。如果我们当年大学挂科了,说不定就是范式这道题没有做好。毕业后,当我们面试时,往往也有关于表设计方面拷问。

        很多时候,我们错误地认为,花费大量时间用在设计上,问题根源在于关系数据库(RDBMS),在于二维表及其之间的联系组成的一个数据组织。而真实的环境中,我们正在大量使用noSQL或者NewSQL,按照目前的趋势(DB-Engines Ranking 得分),将来还会越来越普遍。选用noSQL或者NewSQL 就不需要模式设计了。并且,随着公司、行业数字化程度的加深,智能化触角逐渐延伸,数据量越来越大,结构越来越复杂。 例如现在很火的IOT行业,复杂的业务信息、多样的传输协议、不断升级的传感器,都需要灵活的数据模型来应对。在这种呼唤声中,MongoDB闪亮登场了。MongoDB支持灵活的数据模型。主要体现在以下2点:

        (1)自由模式,无需提前声明、创建表结构,即不用先创建表、添加字段,然后才可以Insert数据。默认情况下MongoDB无需这样操作,除非开启了模式验证。

        (2)键值类型自由,MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。字段值可以包含其他文档,数组及文档数组。

        MongoDB不需要模式设计,其实面对复杂的结构对象,模式的自由带来更大的挑战。

        模式的自由是对数据insert这个动作而言,它去除很多限制了,可以快速讲对象的存进来,并且易于扩展。但是不一定就会带来好的查询性能,好的查询性能还要来自于好的模式设计、来自于好的集合文档的设计。

2、模式设计

        MongoDB可以将模式设计划分为内嵌模式(Embedded)和 引用模式(References)

        内嵌模式

        简单来讲,内嵌模式就是将关联数据,放在一个文档中。例如以下员工信息采用内嵌模式了而存储在了一个文档中:


        引用模式

        引用模式是将数据存储在不同集合的文档中,而通过关系数据进行关联。例如,这里采用引用模式将员工信息存储在了3个文档中,基本信息一个文档,联系方式一个文档,登录权限放在了一个文档中。每个文档之前通过user_id来关联。


3、案例

        下面我们通过一些业务场景,一些具体的案例,来分析、品味一下MongoDB模式设计的选择。

3.1    案例 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"

                    }

        }

        即使用内嵌模式,将数据存储在一个集合中。

3.2    案例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.3    案例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"

        }

3.4    案例 4

        上面三个例子,在关系型数据库中都可以用我们学习过的关系(例如1:1;1:N)来描述,那么我们再举一个关系型数据库难以描述的关系 -- 树状关系

        例如,我们在电商网站上常见的商品分类关系,一级商品、二级商品、三级商品、四级商品关系。我们简化此例子如下:


        那么在MongoDB中可以轻松实现他们关系的查询。

        1)情景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)情景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" } )

4、后记

        MongoDB的模式设计是一个比较大的课题,需要多看看情景案例,多品味一些优秀的文档设计,多问些问什么要这样做,是否有更优的设计,要慢慢去领悟MongoDB的哲学思想。


        

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容

  • 一、MongoDB简介 1.概述 ​ MongoDB是一个基于分布式文件存储的数据库,由C++语言编写。旨在为WE...
    郑元吉阅读 977评论 0 2
  • NoSql数据库优缺点 在优势方面主要体现在下面几点: 简单的扩展 快速的读写 低廉的成本 灵活的数据模型 在不足...
    dreamer_lk阅读 2,718评论 0 6
  • 简介 MongoDB 是一个基于分布式文件存储的NoSQL数据库 由C++语言编写,运行稳定,性能高 旨在为 WE...
    大熊_7d48阅读 36,817评论 1 9
  • 1. MongoDB 简介 MongoDB是一个可扩展的高性能,开源,模式自由,面向文档的NoSQL,基于分布式文...
    rhlp阅读 1,113评论 0 3
  • 生活, 平静如一湾湖水, 游鱼,水草, 阵阵涟漪, 即是喜悦; 但不知什么时候, 人们习惯了狂欢, 喜欢卷起风暴,...
    布里斯班的斑马阅读 266评论 0 0