欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

MongoDB更需要好的模式设计 及 案例赏析

程序员文章站 2023-11-18 20:33:16
一 挑战 设计从来就是个挑战。 当我们第一次接触数据库,学习数据库基础理论时,都需要学习范式,老师也一再强调范式是设计的基础。范式是这门课程中的重要部分,在期末考试中也一定是个重要考点。如果我们当年大学挂科了,说不定就是范式这道题没有做好。毕业后,当我们面试时,往往也有关于表设计方面拷问。 很多时候 ......

一  挑战

设计从来就是个挑战。

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

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

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

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

mongodb不需要模式设计时错误的,其实面对复杂的结构对象,模式的*带来更大的挑战。

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

 

二  模式设计

mongodb可以将模式设计划分为内嵌模式(embedded)和 引用模式(references)

内嵌模式

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

MongoDB更需要好的模式设计 及 案例赏析

引用模式

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

MongoDB更需要好的模式设计 及 案例赏析

 

 三  案例 

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

 

案例 1

 

 假如现在我们描述来顾客(patron)和顾客的地址(address),其er图如下:

MongoDB更需要好的模式设计 及 案例赏析

 

 

 我们可以将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个以上 ( 流泪 ╥╯^╰╥)

MongoDB更需要好的模式设计 及 案例赏析

 

 

 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更需要好的模式设计 及 案例赏析

 

其设计模式可以如下(内嵌模式),将出版社的信息作为一个子文档,来内嵌到书籍的文档中,具体信息如下:

以下书籍《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更需要好的模式设计 及 案例赏析

 

 那么在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的哲学思想。

总之,这是一个多看、多想、多思的蜕变羽化过程,可能时间很长、过程有些痛苦。

 

本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!

本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!