为什么用MongoDB?
传统的计算机应用大多使用关系型数据库来存储数据,比如大家可能熟悉的MySql, Sqlite等等,它的特点是数据以表格(table)的形式储存起来的。数据库由一张张排列整齐的表格构成,就好像一个Excel表单一样,每个表格会有若干列,比如一个学生信息表,可能包含学号、姓名、性别、入学年份、高考成绩、籍贯等等。而表格的每一排,则是一个个学生的具体信息。在企业级应用和前互联网时代,关系型数据库几乎是不二选择。关系型数据库的特点是有整齐划一的组织,很方便对数据进行描述、插入、搜索。
想象有一个传统的网上服装商店吧,它的主要的数据可能是储存在一张叫products的表单里,表单可能包含这些列:商品编号(ID)、名称(Name)、商家(brand)、主目录(cate)、子目录(sub-cat)、零售价(price)、是否促销(promotion)等等。如果有一个用户想要查找所有价格低于300元的正在促销的鞋子的编号和名称,则可以执行类似于以下的SQL语句:
SELECT ID, name FROM products WHERE cate='shoes' AND price<300 and AND promotion=true;
有时会涉及到复杂的跨表查询,需要大量使用join语句。这种跨表查询不仅降低了查询速度,而且这些语句写起来也不简单。
那么,如果用MongoDB数据库来实现,可以如何设计数据模型呢?很简单,像下面这样[1]:
_id: POST_ID
title: TITLE_OF_POST,
description: POST_DESCRIPTION,
author: POST_BY,
tags: [TAG1, TAG2, TAG3],
likes: TOTAL_LIKES,
comments: [
{
user:'COMMENT_BY',
message: TEXT,
dateCreated: DATE_TIME,
},
{
user:'COMMENT_BY',
message: TEXT,
dateCreated: DATE_TIME,
}
]
在MongoDB里,每篇博客文章以一个文档(document)的形式保存起来,而文档内部包含了很多项目,比如title tags
等,每一个项目都是key-value
的形式,即有一个项目的名字,比如title
,以及它的值TITLE_OF_POST
。而重要的是,一个key
可以有多个values
,他们用[]
括起来。
这种“宽松”的数据存储形式非常灵活,MongoDB不限制每个key
对应的values
的数目。比如有的文章没有评论,则它的值就是一个空集,完全没有问题;有的文章评论很多,也可以无限制地插入。更灵活的是,MongoDB不要求同一个集合(collection,相当于SQL的table)里面的不同document有相同的key,比如除了上述这种文档组织,有的文档所代表的文章可能没有likes这个项目,再比如有的文章可能有更多的项目,比如可能还有dislikes等等。这些不同的文档都可以灵活地存储在同一个集合下,而且查询起来也异常简单,因为都在一个文档里,不用进行各种跨文档查询。而这种MongoDB式的存储也方便了数据的维护,对于一篇博客文章来说,所有的相关数据都在这个document里面,不用去考虑一个数据操作需要involve多少个表格。
当然,除了上述的优点,MongoDB还有不少别的优势,比如MongoDB的数据是用JSON(Javascript Object Notation)存储的(就是上面的这种key-value的形式),而几乎所有的web应用都是基于Javascript的。因此,存储的数据和应用的数据的格式是高度一致的,不需经过转换。更多的优点可以查看:[2]。
创建集合和删除集合
在IntelliShell中编写命令,新建一个数据库:
use tutorial #新建tutorial数据库
显示数据库:
show databases
在mongodb中如果数据库中没有数据则不显示该数据库。
我们试着往我们的数据库里添加一个集合(collection)
MongoDB里的集合和SQL里面的表格是类似的:
db.createCollection('author')
顺利的话会显示:
{ "ok" : 1 }
表示创建成功。
你可以再回头执行:
show databases
演示:
//创建数据库studb
use studb;
//创建一个集合
db.createCollection('author')
//显示数据库
show databases;
返回ok
这时候我们的author集合已经位列其中。你可以再执行
show collections
可以看到创建的集合author也在其中。
我们暂时不需要author这个集合,所以我们可以通过执行:
db.author.drop()
来将其删除。这时候你再执行show collections,就再也看不到我们的author了。
这一节要记住的点主要只有一个:集合(collection)类似于SQL的表格(table),类似于Excel的一个个表格。
插入数据
想象一个精简版的“豆瓣电影”。我们需要创建一个数据库,来存储每部电影的信息,电影的信息包括:
- 电影名字
- 导演
- 主演(可能多个)
- 类型标签(可能多个)
- 上映日期
- 喜欢人数
- 不喜欢人数
- 用户评论(可能多个)
- 显然我们需要先创建一个叫电影的集合:
db.createCollection('movie')
然后,我们就可以插入数据了:
db.movie.insert(
{
xanme:"灰猎犬号",
doc:"亚伦-施耐德",
actors:['汤姆·汉克斯','斯蒂芬·格拉汉姆','伊丽莎白·苏'],
pub:new Date(2020,7,10),
commons:[{
uname:'凌睿',
cdate:new Date(2020,7,12),
des:'汤姆·汉克斯把士兵、上尉、列车长、船长、舰长、机长、宇航员、狱警、FBI、工程师、符号专家、报社编辑、律师、制片人、玩具、汽车……全都演了个遍,人生圆满了。',
},
{
uname:'南辕北辙',
cdate:new Date(2020,7,10),
des:'汤叔captain专业户!电影全程无尿点,值得一看'
},]
});
请注意,这里插入数据之前,我们并不需要先声明movie这个集合里面有哪些项目。我们直接插入就可以了~这一点和SQL不一样,SQL必须先声明一个table里面有哪些列,而MongoDB不需要。
把上面的例子复制进命令行应该可以顺利运行,但我强烈建议你手动打一下,或者输入一部你自己喜欢的电影。insert操作有几点需要注意:
- \1. 不同key-value需要用逗号隔开,而key:value中间是用冒号;
- \2. 如果一个key有多个value,value要用[]。哪怕当前只有一个value,也加上[]以备后续的添加;
- \3. 整个“数据块”要用{}括起来;
如果你在insert之后看到WriteResult({ "nInserted" : 1 }),说明写入成功。
这个时候你可以用查询的方式来返回数据库中的数据:
db.movie.find().pretty()
演示:
//创建数据库moviedb
use moviedb;
//创建一个集合
db.createCollection('movie');
//向集合中插入数据
db.movie.insert(
{
xanme:"灰猎犬号",
doc:"亚伦-施耐德",
actors:['汤姆·汉克斯','斯蒂芬·格拉汉姆','伊丽莎白·苏'],
pub:new Date(2020,7,10),
commons:[{
uname:'凌睿',
cdate:new Date(2020,7,12),
des:'汤姆·汉克斯把士兵、上尉、列车长、船长、舰长、机长、宇航员、狱警、FBI、工程师、符号专家、报社编辑、律师、制片人、玩具、汽车……全都演了个遍,人生圆满了。',
},
{
uname:'南辕北辙',
cdate:new Date(2020,7,10),
des:'汤叔captain专业户!电影全程无尿点,值得一看'
},]
});
db.movie.find()
仔细观察find()的结果,你会发现多了一个叫'_id'的东西,这是数据库自动创建的一个ID号,在同一个数据库里,每个文档的ID号都是不同的。
我们也可以同时输入多个数据:
db.movie.insert([
{
xanme:"塘鹅暗杀令",
doc:"亚伦-施耐德",
actors:['朱莉娅·罗伯茨','丹泽尔·华盛顿','山姆·夏普德'],
pub:new Date(1993,12,17),
commons:[{
uname:'山姆·夏普德',
cdate:new Date(2011,2,120),
des:'90年代的中国,在音像店中最常看到的光碟影片之一,以铁盒包装。怕是因为“揭露了美帝的丑恶”吧?',
},
{
uname:'夜丫儿',
cdate:new Date(2012,9,2),
des:'这是部太有名的电影,却一直没看过。现在看来有些太过理想化了吧。'
},
]
},
{
xanme:"惊唇劫 Kiss the Girls",
doc:"亚伦-施耐德",
actors:['摩根·弗里曼','艾什莉·贾德','加利·艾尔维斯'],
pub:new Date(1997,11,13),
commons:[{
uname:'九尾黑猫',
cdate:new Date(2008,8,7),
des:'贾德的悬疑系列片里最好看的一个',
},
{
uname:'滕雅望',
cdate:new Date(2011,12,03),
des:'司法心理学家,某些场景,竟然跟《杀人的回忆》很相似⋯⋯'
},]
},
])
顺利的话会显示:
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
表面我们成功地插入了两个数据。注意批量插入的格式是这样的:db.movie.insert([{ITEM1},{ITEM2}])。几部电影的外面需要用[]括起来。
请注意,虽然collection的插入不需要先声明,但表达相同意思的key,名字要一样,比如,如果我们在一个文档里用directed_by来表示导演,则在其它文档也要保持同样的名字(而不是director之类的)。不同的名字不是不可以,技术上完全可行,但会给查询和更新带来困难。
好了,到这里,我们就有了一个叫moviedb的数据库,里面有一个叫movie的集合,而movie里面有三个记录。接下来我们就可以对其进行查询了。
查询
在上一节我们已经接触到最简单的查询db.movie.find().pretty()。MongoDB支持各种各样的深度查询功能。先来一个最简单的例子,
单个条件查询
找出汤姆·汉克斯主演的所有电影:
db.movie.find({'actors':'汤姆·汉克斯'}).pretty()
查询结果:
多个条件查询
也可以设置多个条件。比如找出亚伦-施耐德导演的, 汤姆·汉克斯主演的电影:
db.movie.find({'actors':'汤姆·汉克斯','doc':'亚伦-施耐德'}).pretty()
查询结果:
这里两个条件之间,是AND的关系,只有同时满足两个条件的电影才会被输出。同理,可以设置多个的条件,不赘述。
条件之间也可以是或的关系,比如查找汤姆·汉克斯主演或亚伦-施耐德导演的电影
db.movie.find({$or : [{'actors' : '汤姆·汉克斯'},{'doc' : '亚伦-施耐德'}]}).pretty();
查找结果:
三个电影都查找出来,因为他们的导演都是亚伦-施耐德。
注意这里面稍显复杂的各种括号。
范围搜索
这里需要在数据库中插入一个属性项,也就是更新操作。
为每个电影添加一个值likes--喜欢人数
db.movie.update({'xanme':'灰猎犬号'},{$set:{'likes':128023}});
更新结果:
那么比如找出50万人以上赞的电影:
db.movie.find({likes:{$gt:110000}}).pretty()
类似地,少于二十万人赞的电影:
db.movie.find({likes:{$lt:200000}}).pretty()
类似的运算符还有:gte:大于或等于;:等于。
注意,对于包含多个值的value的,同样可以用find来查询。比如:
db.movie.find({'标签':'恐怖'}).pretty()
返回 灰猎犬号,假设该对象有多个标签,也会返回。
如果你确切地知道返回的结果只有一个,也可以用findOne:
db.movie.findOne({'xanme':'灰猎犬号'})
如果有多个结果,则会按磁盘存储顺序返回第一个。请注意,findOne()自带pretty模式,所以不能再加pretty(),将报错。
如果结果很多而你只想显示其中一部分,可以用limit()和skip(),
limit(m):指明输出的个数m
skip(n):指明从第n+1个结果开始数
常用于分页功能。
db.movie.find().limit(2).skip(1).pretty()
则跳过第一部,从第二部开始选取两部电影。
局部查询
第五节的时候我们讲了find的用法,但对于符合条件的条目,我们都是返回整个JSON文件的。这类似于SQL里面的SELECT *。有的时候,我们需要的,仅仅是部分数据,这个时候,find的局部查询的功能就派上用场了。
//查询喜欢大于110000的电影的导演和电影名
db.movie.find({likes:{$eq:110000}},{'doc':1,'xanme':1})
结果:
这里find的第二个参数是用来控制输出的,1表示要返回,而0则表示不返回。默认值是0,但_id是例外,因此如果你不想输出_id,需要显式地声明:
db.movie.find({likes:{$eq:110000}},{'doc':1,'xanme':1,'_id':0})
结果:
更新
很多情况下你需要更新你的数据库,比如有人对某部电影点了个赞,那么你需要更新相应的数据库。比如有人对《灰猎犬号》点了个赞,而它本来的赞的个数是128000,那么你需要更新到128001。可以这样操作:
//将电影名为灰猎犬号的电影的喜欢人数改为128001,如果集合数据中没有该属性则自动创建并赋值。
db.movie.update({'xanme':'灰猎犬号'},{$set:{likes:128001}})
第一个大括号里表明要选取的对象,第二个表明要改动的数据。请注意上述的操作相当不现实,因为你首先要知道之前的数字是多少,然后加一,但通常你不读取数据库的话,是不会知道这个数(134370)的。
上述的set方法还可以再集合中国创建属性,并赋值。
MongoDB提供了一种简便的方法,可以对现有条目进行增量操作。假设又有人对《七宗罪》点了两个赞,则可以:
db.movie.update({'xanme':'灰猎犬号'},{$inc:{likes:2}})
注意如果有多部符合要求的电影。则默认只会更新第一个。如果要多个同时更新,要设置{multi:true},像下面这样:
db.movie.update({}, {$inc:{likes:10}},{multi:true})
但是前提是你要改变的数据必须数据类型一致。
注意,以上的更新操作会替换掉原来的值或者创建数据属性,但是如果本来有一个属性,对应了多个值。
所以如果你是想在原有的属性列表中基础上增加一个值的话,则应该用$push
比如,为《灰猎犬号》添加一个演员。
db.movie.update({'xanme':'灰猎犬号'},{$push:{actors:'李钊'}})
结果:
删除
删除的句法和find很相似,比如,要删除标签为romance的电影,则:
db.movie.remove({'xanme':'灰猎犬号'})
考虑到我们数据库条目异常稀少,就不建议你执行这条命令了~
注意,上面的例子会删除所有标签包含romance的电影。如果你只想删除第一个,则
db.movie.remove({'xanme':'灰猎犬号'},1)
如果不加任何限制:
db.movie.remove({})
会删除movie这个集合下的所有文档。
网友评论