您的位置:首页 > 数据库 > Mongodb

对MongoDB设计模式的理解和常用查询总结

2017-08-27 18:57 127 查看

一、MongoDB的特点

mongo的特点一图以蔽之:



先说说关系型数据库:

这里的“关系”究竟指的是什么?我们不说大概念和纯理论,只说最实在的东西——关系型数据库中的“表”代表着一类“实体”,实体之间会存在一定关系,比如用户实体和订单实体,一个用户可以有多个订单,而一个订单只属于一个用户,这就是一对多的关系。比如商品和类别,一个商品可以归属多个类别,而一个类别下会有多个商品,这就是多对多的关系。

在关系型数据库中,通过在表中添加存储外键的列来表示这些关系。“主键”阿“外键”阿这些键,本质上就是对一个实体的引用,在一个实体中存储了另一个实体的引用,这样的话,实体之间的关系信息是不是也就有了呢?

这样在查询时,通过主键和外键的关联,就可查询实体与实体间的关系。在修改实体时,由于实体之间的关系仅用引用来表示,所以实体的信息仅存在一处,而引用是不会变的不用修改的,这样也只需修改一处的实体信息即可。

再说说mongo:

mongo的设计目标可不仅仅是简单的键值存储,它也可以做到表达现实世界中纷繁复杂的实体和实体之间的关系,所以它也必须能够处理一对多多对多的关系,那它是如何做到的呢?

mongo中一个实体由一个文档(document)表示。文档的属性值可以是基本类型和引用,就和RDBMS的表的列的值一样,但与之不同的是,文档的属性值还可以是:

(1)另一个完整的文档而非引用

(2)数组,且数组的元素既可以是基本类型和引用,也可以是一个完整的文档

mongo可以利用文档中的数组,无比直观地表示一对多多对多的关系。当然也可以像RDBMS那样,在一个文档中存储另一个文档的键来表示这种关系,只不过不能像RDBMS可以通过基于join的连接查询一次查出这种关系,mongo必须经过两次查询(第一次查出符合查询要求的实体,第二次查出与该实体有关系的其它实体)。

同时由于可以值可以是另一个完整的文档而非引用,使其可以设计反范式的数据模型。

二、mongoDB的设计模式

2.1、嵌入与引用

嵌入:包括文档的属性值是一个完整文档,也包括文档的数组属性的元素是一个完整的文档。

引用:就是指文档的属性值是另一个文档id,或文档的数组实行的元素是另一个文档id。

嵌入是反范式的,而引用是符合范式的设计。

嵌入提供了一些查询的性能优势,而引用提供了更多的灵活性。

选择规则:当子文档从不会出现在父文档以外的环境中时使用嵌入方式,否则在单独的集合里存储子文档。

2.2、一对多

(1)像RDBMS那样,在一个文档中存储另一个文档的键来表示这种关系

(2)利用文档的数组,数组的元素是另一个完整的文档

(3)利用文档的数组,数组的元素是另一个文档的id

2.3、多对多

(1)利用文档的数组,数组的元素是另一个文档的id

2.4、树

树的本质是“级联的一对多关系”。要查询树的话,常规做法是递归地进行一对多的查询。树形数据结构并不是mongo所擅长的,但可以通过一种冗余数据的技巧——物化路径的方式,简化树的查询。

遵循物化路径模式,每个树节点都包含一个path字段,这个字段存储了每个级联节点祖先的ID,根节点path为空。

//根节点
{
_id: ObjectId("000");
path: null;
}
//一级节点
{
_id: ObjectId("111");
path: 000;
}
//二级节点
{
_id: ObjectId("222");
path: 000:111;
}
//...


这样在查询特定子树时,比如要查询上述一级节点下的子树,只需 find({path: /^000/}),要查询上述二级节点下的子树,只需 find({path: /^000:111/}),都只要查询一次即可。但如果要修改和删除树的某一节点,就需要修改该节点下所有节点的path,这就是反范式设计的代价。

三、mongoDB的查询

mongo有个特色,它标榜为文档数据库,我们只能将一个个文档存入其中。此外,mongo还使用文档来表示命令,比如查询条件命令、更新操作命令、参数设置命令都是一个个文档。

3.1、简单查询

(1)范围查询符

$lt、$gt、$lte、$gte、$ne,后接基本类型,且仅当它们含有相同类型的值时才进行比较,

$exsit,后接布尔类型

db.users.find({'birth_year': {'$gt': 1985, '$lte': 2015}})
db.users.find({'birth_year': {'$exist': true}})


(2)布尔运算符

$not,后接文档命令,

$nor、$and、$or,后接文档命令数组

db.users.find({'age': {'$not': {'$lte': 30}}})
db.users.find({
'$or': [
{'age': '30'},
{'level': 'VIP'}
]
})


(3)点操作符

mongo中的点操作符非常强大,它不仅可以深入内嵌对象的内部进行查询,也可以像操作一个对象一样去操作对象数组。

{
_id: ObjectId("xxx"),
address: {
name: 'home',
city: 'NanJing'
}
}
//查询住址在南京的用户
db.users.find({'address.city': 'Nanjing'});

//------------------手动分割----------------------

{
_id: ObjectId("xxx"),
address: [
{
name: 'home',
city: 'Nanjing'
},
{
name: 'work',
city: 'BeiJing'
}
}
//查询所有住址中有在南京的用户
db.users.find({'address.city': 'Nanjing'});
//查询所有住址都不在南京的用户
db.users.find({'address.city': {$ne:'Nanjing'}});
//查询第一个住址在南京的用户
db.users.find({'address.0.city': 'Nanjing'});
//查询家庭住址在南京的用户
db.users.find({
'address': {
'$eleMatch':{
name: 'home',
city: 'Nanjing'
}
}
});


(4)数组操作符

mongo可以利用数组表示实体之间的一对多和多对多的关系,所以对数组操作的支持程度,影响着mongo查询基于数组的实体关系的便利度。上面的点操作符在很大程度上给予了数组操作方便,除此之外还有一些:

$eleMatch,如果提供的所有的查询条件在相同的子文档中,则匹配,

$size,如果子文档数组大小与提供的数值相同,则匹配,

$in,如果任意参数在引用集合里,则匹配,

$all,如果所有参数在引用集合里,则匹配,

$nin,如果没有参数在引用集合里,则匹配,

以上三个操作符都必须后接数组,但应用的属性既可以只拥有一个值,也可以拥有多个值(数组)

唯一比较遗憾的就是在mongo中无法直接查询数组的大小,只能使用$size操作符给定一个值去判断数组的大小等不等于该值。我也实在不明白mongo为何不提供这个功能。

(5)JavaScript查询运算符

以上都是mongo提供给你的查询操作,你还可以自己编写js脚本代码来定制你自己的查询操作。

db.getCollection('emrRecord').find({
'$where': "function() { return this.医院编码 != '1002'}"
})

db.getCollection('emrRecord').find({
'$where': "this.医院编码 != '1002'"
})


就如同js中的eval()函数一样强大,但要注意js注入攻击的可能性和性能损失问题。

(6)定位选择返回文档中的一部分

db.getCollection('emrRecord').find({})
.skip(100),
.limit(200),
.sort({住院号:1, 病历名称:1})


虽然skip和limit可以实现分页查询,但是当skip的值非常大的时候,mongo执行的效率是非常低的。因为它会非常蠢的去依次扫描跳过skip参数指定数量的文档。另外,调用skip、limit和sort的顺序并不重要。

(7)选择返回文档的子集

以上的所有查询都相当于在 select *,能不能 select name, age,只选择想要的属性呢?当然可以:

//返回的文档只包含name属性
db.users.find({}, {'name': 1});
//返回的文档不包含name属性
db.users.find({}, {'name': 0});
//返回的文档的address数组只包含前三条
db.users.find({}, {'address': {'$slice': 3}});
//返回的文档的address数组只包含前最后一条
db.users.find({}, {'address': {'$slice': -1}});
//返回的文档的address数组只包含第三到第五条
db.users.find({}, {'address': {'$slice': [2,5]}});


3.2、聚合查询

聚合框架常用操作符:

db.collection.aggregate([
{$project: ...},
{$match: ...}, //上节中所有的查询条件命令文档都可以作为$match的值
{$unwind: ...}, //将文档中通过数组表示的一对多关系,转换成形似关系型数据库中的表现形式
{$group: ...},
{$skip: ...},
{$limit: ...},
{$sort: ...},
{$out: ...}, //将管道的输出结果保存到指定集合中。若集合不存在则会创建一个,若存在则会完全取代现有集合
])


(1) $project

具有文档重塑功能:

//返回的文档只有areaCode字段
db.getCollection('patient').aggregate([
{
$project: {areaCode: 1}
}
])

//将areaCode重命名为地区编码
db.getCollection('patient').aggregate([
{
$project: {地区编码: '$areaCode'}
}
])

//将first_name和last_name字段的组合重塑为一个全新的文档
db.getCollection('patient').aggregate([
{
$project: {name: {first: '$first_name',
last: '$last_name'}}
}
])


字符串重塑:

db.getCollection('patient').aggregate([
{
$project: {name: {$concat: ['$first_name', ' ', '$last_name']}, //字符串拼接
firstInitial: {$substr: ['$first_name', 0, 1]}, //获取子串
usernameUpperCase: {$toUpper: '$username'}} //大小写转换
}
])


日期重塑:

db.getCollection('patient').aggregate([
{
$project: { 一年中的某一天: {$dayOfyear: '$出院时间'},
一月中的某一天: {$dayOfMonth: '$出院时间'},
一周中的某一天: {$dayOfWeek: '$出院时间'},
日期的年份: {$year: '$出院时间'},
日期的月份: {$month: '$出院时间'},
一年中的某一周: {$week: '$出院时间'},
日期中的小时: {$hour: '$出院时间'},
日期中的分钟: {$minute: '$出院时间'},
日期中的秒: {$second: '$出院时间'},
日期中的毫秒: {$millisecond: '$出院时间'}}
}
])


集合重塑:

db.getCollection('basUser').aggregate([
{
$project: {并集: {$setUnion: ['$病区列表', '$科室列表']},
交集: {$setIntersection: ['$病区列表', '$科室列表']},
差集: {$setDifference: ['$病区列表', '$科室列表']},
是否为子集: {$setIsSubset: ['$病区列表', '$科室列表']},
是否完全相同: {$setEquals: ['$病区列表', '$科室列表']}}
}
])


(2) $group

$group中必须有_id字段以定义按什么字段来分组。当为分组定义_id字段时,可以使用一个或者多个存在的字段:

//根据一个字段进行分组
db.getCollection('patient').aggregate([
{
$group: {
_id: '$科室ID',
count: {$sum: 1}
}
}
])

//根据多个字段进行分组
db.getCollection('patient').aggregate([
{
$group: {
_id: {科室ID: '$科室ID', 病区ID: '$病区ID'},
count: {$sum: 1}
}
}
])


在 $group内使用的函数:

db.getCollection('patient').aggregate([
{
$group: {
_id: '$科室ID',
组内某字段的和: {$sum: '$age'},
组内某字段的平均值: {$avg: '$age'},
组内某字段的最大值: {$max: '$age'},
组内某字段的最小值: {$min: '$age'},
组内某字段的第一个值: {$first: '$name'},  //当有前缀$sort才有意义
组内某字段的最后一个值: {$last: '$name'}, //当有前缀$sort才有意义
组内某字段的非重复值组成的数组: {$addToSet: '$科室名称'},
组内某字段的所有值组成的数组: {$push: '$科室名称'}
}
}
])


参考书籍

《MongoDB 实战》 —— Kyle Banker等
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  mongodb 数据库