zoukankan      html  css  js  c++  java
  • MongoDB:最受欢迎的文档型数据库

    MongoDB介绍

    MongoDB是一个存储文档的非关系型数据库,它并不像mysql、Oracle那样存储结构化的数据,而是存储文档。这个文档可以看成是python中的字典,或者golang中的map。我们来和mysql对比一下。

    可以看到,MongoDB中的集合对应mysql中的表,表中可以有多条记录,那么一个集合里面也可以有多个文档。MongoDB的文档和mysql中的记录是对应的,这个文档格式叫做bson,它是json格式的一个扩展,我们后面会介绍。而且MongoDB还有一个最大的特点就是,同一个集合里面的文档可以有完全不同的字段,这是和关系型数据库的最大区别。同时MongoDB里面并不存在提前指定好的数据格式,如果我们需要增加一个新字段,那么只需要把包含新字段的文档写进MongoDB的集合里面就行了。

    安装MongoDB

    MongoDB的安装比较麻烦,但是不要怕,我们有docker,不熟悉docker的朋友可以去我之前发过的文章里面去找,尽管docker安装MongoDB很简单,但我们还是需要一些docker知识的。另外这个是在linux上运行的,Windows如何安装不讲解。

    (1) 执行命令docker pull mongo:4,表示拉取MongoDB镜像,后面的:4是一个标签,表示版本是4.0的,这是比较新的版本,我们之后就学习这个最新版,其实如果不加:4,默认会拉取最新版本。

    (2) 执行命令docker images,查看拉取的镜像

    我们看到拉取成功,TAG表示标签,版本为4。至于上面的nginx是我之前用的,不用管它。

    (3) 执行docker run --name my_mongo -v /data/mongodb_data:/data/db -d mongo:4,表示根据mongo:4镜像创建一个容器。这个命令有点长,我来解释一下,其实根据镜像创建容器,直接docker run 镜像名即可,比如我们这里docker run mongo:4即可,中间的那一大堆属于附加参数。--name my_mongo表示生成的容器名字叫做my_mongo。-v /data/mongodb_data:/data/db表示宿主机的/data/mongodb_data目录和容器的/data/db目录保持同步,/data/db是MongoDB存储数据的目录,我们让宿主机的一个目录和其保持同步,这样即使容器关闭了,我们依旧可以通过操作/data/mongodb_data来操作数据。-d表示后台运行,不然我们的xshell远程一旦关闭,服务就停了,这显然不是我们想要的结果。

    此时我们/data下面是没有任何东西的,然后我们执行命令

    我们看到容器已经后台启动了,并且名字就叫做my_mongo,只不过我们截图没有截全,有点长,而返回的那一大长串是容器的id,我们可以通过容器的id定位到某一个容器,有人觉得id这么长,通过id定位不是折磨人吗,所以我们才要起一个名字呀,而且id不需要全部输入,只需要输入前几位就可以了。而且我们查看/data目录,会发现mongodb_data目录已经被docker自动帮我们创建了。

    并且这个目录里面还有很多东西,这些东西哪儿来的,显然是容器的/data/db目录同步过来的,因为这两个目录是要保持一致的。如果我把宿主机的目录/data/mongodb_data里面的东西给删除了,那么显然容器里面的/data/db也空了,当然如果了解docker的话,这些都是废话。

    管理MongoDB数据库

    我们安装了MongoDB,那么我们肯定希望管理数据库,这个时候可以使用mongo-express,同样我们使用docker安装。docker pull mongo-express

    镜像下载成功,那么我们就启动这个镜像,docker run --link my_mongo:mongo --name mongo_exp -p 8081:8081 -d mongo-express,我们看到--link my_mongo:mongo,这个选项代表我们需要使用name为my_mongo的容器,:mongo相当于起一个别名,这个选项相当于一种关联,否则mongo-exopress启动之后它管理谁啊。-p 8081:8081,第一个8081是容器暴露出来的端口,因为我们是要通过外部来访问的,需要把端口暴露出来,第二个8081是容器内部使用的端口,一个容器可以看成是一个小型的centos,那么我们在外部通过容器暴露出来的8081端口,就可以访问容器里面端口为8081的服务了,容器里面的mongo-express默认端口是8081,至于外部暴露出来的端口不是8081也无所谓,只要它指向了容器内部监听的8081,我们就能够访问到。比如-p 8088:8081也是可以的,只不过此时我们在外部就需要8088端口来访问了,但是一般两个端口是一致的。我们来执行一下命令:

    启动成功,我们来通过web界面查看一下,通过8081端口。

    我们看到了mongo-express界面,当然前期我们并不需要操作这个,但是可以提前看一下。我们看到MongoDB默认是有admin,config,local这个三个数据库的,这三个数据库不要动,我们后面会新建数据库,然后在我们自己创建的数据库上面做操作。至于数据里面长什么样,默认有哪些collection,可以自己点击进去查看。

    运行mongo shell

    我们可以启动一个mongo shell,也就是交互式界面。通过docker exec -it my_mongo mongo,这里面的my_mongo就是我们之前的容器名,如果创建时不通过--name指定一个名字,那么也可以通过容器返回的id来进行启动,但是输入会不方便。

    另外这个mongo shell是一个JavaScript客户端界面,就是说在mongo shell里面是可以接收js语法的。

    我们看到启动成功了。

    MongoDB基本操作CRUD

    学习任何一个数据库都要学习C(Create)R(Read)U(Update)D(Delete),不过学习之前我们来看看一些相关概念。

    文档主键_id

    每个文档都有一个专属的唯一值,叫做文档主键,这个值存储在_id字段里面。这个字段是MongoDB的每一个都必备的字段,除了数组,MongoDB支持的所有数据类型都可以被当做主键。其实最方便的还是让MongoDB自动帮我们生成主键,在文档中如果不指定主键,那么MongoDB会自动帮我们生成主键,也就是对象主键(ObjectId)。这是默认的文档主键,可以快速生成的12个字节的id,而前4个字节一般是创建时间。

    创建数据库

    另外在MongoDB中创建一个数据库,直接使用use 数据库名即可,没有会自动创建并切换、存在则切换。

    删除数据库

    删除数据库则是,想删除哪个数据库就切换到哪个数据库,然后执行db.dropDatabase()即可。

    创建集合

    db.createCollection(name, options)
    

    name: 要创建的集合名称,options: 可选参数, 指定有关内存大小及索引的选项

    | capped      | 布尔 | (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 当该值为 true 时,必须指定 size 参数。 |
    | ----------- | ---- | ------------------------------------------------------------ |
    | autoIndexId | 布尔 | (可选)如为 true,自动在 _id 字段创建索引。默认为 false。   |
    | size        | 数值 | (可选)为固定集合指定一个最大值,以千字节计(KB)。如果 capped 为   true,也需要指定该字段。 |
    | max         | 数值 | (可选)指定固定集合中包含文档的最大数量。                   |
    

    删除集合

    切换到相应的数据库,使用db.<collection>.drop()即可,
    <collection>是集合的名字
    

    创建文档

    语法:
    db.<collection>.insertOne(
    	<document>,
    	{
    		writeConcern: <document>
    	}
    )
    
    <collection>是你的集合名,集合不存在会默认创建
    <document>是你要写入的文档
    
    这里面的writeConcern则定义了本次文档创建操作的安全写级别
    简单来说,安全写级别用来判断一次数据库写入操作是否成功
    安全写级别越高,丢失数据的风险就越低,然而写入操作的延迟也可能更高
    如果不提供writeConcern,MongoDB则使用默认的安全写级别
    
    当前不用管这个writeConcern,我们后面介绍
    

    insertOne表示插入单条文档,还可以使用insertMany,插入多条文档。多个文档放在一个数组里面。

    db.girls.insertMany(
    	[
    		{
    		"_id": "girl2", "name": "koishi",
    		"age": 15, "gender": "female"	
    		},
    		
    		{
    		"_id": "girl3", "name": "scarlet",
    		"age": 400, "gender": "female"
    		}
    	]
    )
    

    我copy的时候,格式会有点问题,但是无所谓,我们看到多条数据是可以同时插入进去的。

    如果我们插入数据的时候,出现了错误怎么办?首先插入单条数据失败了,那么数据肯定不会在集合里面。但如果插入多条数据失败了呢?

    db.girls.insertMany(
    	[
    		{
    		"_id": "girl4", "name": "tomoyo",
    		"age": 19, "gender": "female"	
    		},
    		
    		{
    		"_id": "girl4", "name": "nagisa",
    		"age": 21, "gender": "female"
    		},
    		
    		{
    		"_id": "girl5", "name": "kurisu",
    		"age": 18, "gender": "female"
    		},
    	]
    )
    
    目前集合里面是有三条记录(准确来说应该叫文档,但是无所谓啦,知道我说什么就行)的,id分别为girl1,girl2,girl3
    我们我们再插入三条,第一条肯定没问题,但是第二条和第一条的主键冲突了,肯定会失败,而第三条如果单独插入肯定是成功的,但是现在它在第二条后面
    所以我们这里主要是想看看MongoDB的策略,如果一次插入多条,那么中间出现了失败,MongoDB会采取什么策略了。
    1.遇到失败也无所谓,依旧会尝试插入所有文档,只要能成功插入,就插入进去,对应我们这里会把第一条和第三条插入进去。
    2.整体是一个事务,如果当中出现错误,那么所有文档都不会插入进去,这里面三条都不会插入到集合里
    3.不是事务,按照数据的顺序一个一个插入,如果遇到失败就停止,对应这里,则是只把第一条插入进去
    

    不出所料,在插入第二条的时候报错了,因为girl4这个主键已经存在了,那么其他文档呢?在这里可以使用db.girls.find()查看当前集合的所有文档

    我们发现此时有4条文档,说明刚才插入的三条,只有第一条插入进去了。因此结论是第3个,按照数组里面文档的顺序依次插入文档,如果出现错误,停止插入。

    更新文档

    db.<collection>.update(
       <query>,
       <update>,
       {
         upsert: <boolean>,
         multi: <boolean>,
         writeConcern: <document>
       }
    )
    
    • query : update的查询条件,类似sql update查询内where后面的。
    • update : update的对象和一些更新的操作符(如(,)inc...)等,也可以理解为sql update查询内set后面的
    • upsert : 可选,这个参数的意思是,如果不存在满足update条件的文档,是否插入objNew,true为插入,默认是false,不插入。
    • multi : 可选,mongodb 默认是false,只更新找到的第一条文档,如果这个参数为true,就把按条件查出来多条文档全部更新。
    • writeConcern :可选,安全写级别。

    比如我们把"_id"为"girl1"的文档的name改成"mashiro"

    db.girls.update({"_id": "girl1"}, {$set: {"name": "mashiro"}})
    

    可以看到成功修改,如果有多个条件,多个修改,那么就在{}写上多个键值对即可。

    我们还可以修改多条文档

    db.girls.update({"gender": "female"}, {$set: {"gender": "女"}}, {"multi": true})
    不要忘记指定multi:true
    

    但是问题又来了,我们这里的查询条件是相当于,如果我想找大于、或者小于的怎么办?比如这里我想把age大于16的文档的gender由"女"变成"女青年"

    db.girls.update({"age": {"$gt": 16}}, {$set: {"gender": "女青年"}}, {"multi": true})
    我们看到如果是等于16,那么直接写16
    但如果是大于16,需要再套上一层{},里面写"$gt": 16,另外别忘记multi: true
    关于$gt表示MongoDB的操作符,熟悉其他语言的应该不用我说,这些操作符我们后续在查询文档的时候会一一介绍,另外"$gt"也可以写成$gt,这个引号加与不加都无所谓
    

    除了update之外,MongoDB还提供了updateOne和updateMany两个api表示更新一条、和多条满足条件数据,这两个api的前两个参数和update是一样的,可以看做没加multi: true和加了multi: true的update。

    另外MongoDB还提供了save方法,这里面传入一个文档即可,如果id存在,那么直接把整个文档替换掉。

    我们看到我新创建的文档,_id也叫girl1,那么直接就把原来的给换掉了,而且我们发现field不一样也无所谓。

    如果_id不存在,那么就创建,存在则替换。

    删除文档

    插入文档:insertOne、insertMany,更新文档:updateOne、updateMany,那么删除文档同样:deleteOne、deleteMany

    db.girls.deleteOne({"age": {"$gt": 16}})  
    删除第一条age大于16的数据
    

    db.girls.deleteMany({"age": {"$gt": 10}})  
    删除所有age大于10的数据
    

    我们看到,确实大于10的全部删完了,但是对于没有age的则还保留。

    另外我们这里又插入了一条数据,name为15是一个int,可以看到,即便是同一个field,不同的文档,那么类型是可以不一样的,然后我们删除name大于10的文档。确实删掉了,但是有两个文档的name是字符串,无法比较,那么在MongoDB中,无法比较的则默认不满足条件,因此这里会保留。

    查询文档

    查询文档我们知道可以通过db.girls.find()来查找,这个是查找全部的文档,这里面是可以加条件的。

    db.<collection>.find(query, projection)
    
    query :可选,使用查询操作符指定查询条件
    projection :可选,返回指定的field
    

    数值比较

    其余的可以自己尝试

    and

    如果我们指定多个条件也是可以的,比如筛选出age > 16并且gender="female"的。

    db.girls.find({"age": {"$gt": 16}, "gender": "female"})
    直接把两个条件写在一起即可
    
    另外如果要查找age > 16 并且 age < 19的,不能这么写
    db.girls.find({"age": {"$gt": 16}, "age": {"$lt": 19}})
    应该这么写
    db.girls.find({"age": {"$gt": 16, "$lt": 19}})
    
    如果按照第一种写法的话,那么只以第二个"age": {"$lt": 19}为条件,很好理解
    {"age": {"$gt": 16}, "age": {"$lt": 19}}想象成一个python中的字典,key不能重复,所以第二个就把第一个元素给覆盖了。
    

    第一个筛选的结果就是age < 19的,第二才是age > 16 and age < 19

    or

    如果是or的话,那么我们就需要手动指定了,比如我们筛选age < 17 or age > 19

    db.girls.find(
    	{
    		"$or": [
    			{"age": {"$gt": 19}},
    			{"age": {"$lt": 17}}
    		]
    	}
    )
    

    可以和and进行组合,比如我们要筛选(age < 17 or age > 19) and gender="女青年"的,这里我已经把数据给改了,增加了一个gender字段

    db.girls.find(
    	{
    		"$or": [
    			{"age": {"$gt": 19}},
    			{"age": {"$lt": 17}}
    		],
    		"gender": "女青年"
    	}
    )
    

    如果我们要筛选(age > 16 and age < 21) or gender="女青年"

    db.girls.find(
    	{
    		$or: [
    			{"age": {"$gt": 16, "$lt": 21}},
    			{"gender": "女青年"}
    		]
    	}
    )
    

    个人觉得MongoDB设计的查询方式好费劲啊

    projection

    db.<collection>.find(query, projection)
    
    query :可选,使用查询操作符指定查询条件
    projection :可选,返回指定的field
    

    还记得这个projection吗?我们来演示一下

    里面没有关键字参数,那么想输入projection,并且又没有查询条件的话,那么查询条件直接指定一个{}即可。我们看到指定了name:1,那么只返回_idname,里面填指定的field,为1表示返回,为0表示不返回。注意:只能填1或者0,并且如果填入1则都要填1,填入0则都要填0。比如:

    如果只返回name,那么就{"name": 1}即可,这就代表只返回name,当然还有id
    不能出现{"name": 1, "age": 0}这种情况,这种情况程序员的理解是返回name不返回age,但是MongoDB认为这是不合法的语法
    如果是只返回name不返回age,直接{"name": 1}即可
    
    如果不返回age,那么就是{"age": 0},0表示不返回,那么会age给扔掉,返回其它的。总之不能既出现1又出现0,当然有一种特例,那就是id
    除了id之外,projection指定的所有字段要么都是1,要么都是0。一旦指定了,只返回指定为1的,或者不返回指定为0的。
    但是id是个特例,因为它是自动返回的,我们指定"name": 1,应该只返回name的,但是id、不对应该是_id,也返回了。这个时候我们可以写{"_id": 0, "name": 1},这时候就把_id也扔掉了
    

    模糊查询

    db.girls.find({"name": /ri/})  查找name包含字符串ri的文档
    db.girls.find({"name": /^ri/})  查找name以字符串ri开始的文档
    db.girls.find({"name": /ri$/})  查找name以字符串ri结尾的文档
    

    查询指定类型

    我们知道MongoDB里面,不同文档的field可以有不同的类型。

    如果我想筛选出,指定类型的文档怎么办呢?比如我这里要筛选出name为字符串的

    db.girls.find({"name": {$type: "string"}})
    

    limit与skip

    limit就是关系型数据库的limit,skip当成是offset即可。

    如果只有skip没有limit

    sort排序

    按照谁排序,就指定谁,1是正序,-1为倒序。我们注意到:如果没有相应字段、或者该字段不是整型,那么升序排,默认排最上面,倒序排默认排最下面。

    有时候我们会先使用sort排序,然后再使用limit选取。这个和关系型数据库一样,先执行sort(order by),再执行skip(offset),然后执行limit(limit)

    数组操作符

    我们的field除了对应字符串、整型之外,还可以是数组。

    我们插入几条数据,新建一个集合。name是姓名,info是一个数组,分别是电话、薪水、国籍、爱好(也是数组)
    db.boys.insertMany(
    	[
    		{"name": "boy1", info: [18538126548, 8000, "china",["篮球", "舞蹈", "音乐"]]},
    		{"name": "boy2", info: [12853146598, 10000, "japan",["滑冰", "舞蹈", "唱歌"]]},
    		{"name": "boy3", info: [11545815888, 9000, "america",["台球", "漫画"]]},
    		{"name": "boy4", info: [13654158865, 13000, "china",["rap", "篮球", "音乐"]]},
    		{"name": "boy5", info: [12541442654, 9500, "japan",["蹦极", "吸烟", "烫头"]]}
    	]
    )
    

    我们来查找,info中出现china和8000的。

    $all操作符表示info里面必须出现"china"和8000
    db.boys.find({
    	"info": {$all: ["china", 8000]}
    })
    

    还有$elemMatch,表示或者,
    
    db.boys.find({
    	"info": {$elemMatch: {$gte: 10000, $lte: 13000}}
    })
    
    查询薪水大于等于10000并且小于13000的,不过可能有人好奇,你怎么知道这个查的是薪水,数组还有其它元素呢?
    所以这是elemMatch,只要有一个元素能够满足在10000到13000之间就行,不然它怎么能表示或者呢。
    所以我们发现这里面的条件明明是and,但我们却说这是"或者"。因为对于数组来说,and和or不是针对于条件来的,而是针对于数组里面的元素。
    如果是all,则要求都满足。elemMatch只要数组里面有一个元素满足即可。
    

    筛选出国籍不为america同时薪水大于9000小于12000的,
    db.boys.find(
    	{
    		"info": {
    			$all: [
    				{$elemMatch: {$ne: "america"}},
    				{$elemMatch: {$gt: 9000, $lt: 12000}}
    			]
    		}
    	}
    )
    
    只要info里面不出现america,并且出现9000~12000的元素,那么就匹配了
    

    in操作符

    筛选出name为boy1 boy3 boy5的
    
    db.boys.find(
    	{"name": {$in: ["boy1", "boy3", "boy5"]}}
    )
    

    这个in里面还可以使用正则,我们再插入几条数据。

    选择name以b开头的和r开头的
    
    db.boys.find(
    	{
    		"name": {$in: [/^b/, /^r/]}
    	}
    )
    

    忽略大小写,/^r/ --> {$regex: /^r/, $options: "i"}  i表示ignore,忽略大小写
    
    db.boys.find(
    	{
    		"name": {$regex: /^r/, $options: "i"}
    	}
    )
    

    文档游标

    我们之前说,db.<collection>.find()是返回所有数据,其实它返回的是一个文档迭代游标,在不遍历游标的前提下,只会列出20个文档。由于我们目前的文档数小于20个,所以就全部展示了。

    既然返回了一个游标,我们可以使用一个变量将其保存起来,之前说了mongo shell是支持js语法的,按照js语法来就行。

    var cursor = db.boys.find()
    可以使用游标的下表来获取指定文档,比如获取第二个文档
    cursour[1]
    

    当我们遍历完所有的数据,游标就会关闭,或者10分钟之后,游标会自动关闭。你可以使用var = cursor = db.boys.find().noCursorTimeout()来保证游标一致有效,不过之后当你不需要使用游标了,你需要调用cursor.close()手动关闭游标。

    游标支持以下函数:

    1. coursor.hasNext():当前游标的后一个位置是否还有文档,有为true,没有为false

    2. cursor.next():得到下一个文档

    var cursor = db.boys.find();
    while (cursor.hasNext()){
    	print(cursor.next());
    }
    

    这里没有显示具体数据,而是一个bson对象,我们可以使用printjson打印,但是会比较长,我不好截图,于是就直接print了。总之知道这两个函数的作用即可。

    3. cursor.forEach(<function>):对每一个文档都执行<function>函数

    4. limit、sort、skip:之前介绍过了,另外limit(0)表示相当于没有limit,也就是全部返回

    5. curosr.count():计算数量

    但是需要注意的是,这个count是不会考虑limit和skip的,如果指定了limit,db.boys.find().limit(3).count()这里返回的依旧是8,因为它不会考虑count,如果考虑的话,只需要在count函数里面加上参数true即可。

    还有一点需要注意:我们这里的find没有指定筛选条件,那么count是从集合的元数据metadata中取得结果,只有当指定了筛选条件的时候,才会真正计算。所以当数据库分布式结构比较复杂的时候,元数据中文档数量可能不准确,这时候应该避免使用没有筛选条件的count,而是使用聚合管道来计算文档数量,这个聚合管道后面会介绍。

    还记得我们之前说的projection吗?指定返回的字段,我们当时遗漏了一点,那就是数组。

    $slide: 1表示返回数组的前1个元素
    $slide: 1表示返回数组的后1个元素
    $slide: [1, 3]表示跳过1个元素,返回接下来的三个元素
    
    db.boys.find(
    	{},
    	{"name": 1, "info": {$slide: 2}, "_id": 0}
    )
    
    结合elemMatch,注意这里并不是"返回大于america的记录",而是全部返回,因为find第一个参数是{}
    这里elemMatch指的是,数组中,大于"america"的元素返回。这里比较会按照字符的ascii码一个一个比较
    db.boys.find(
    	{},
    	{"name": 1, "info": {$elemMatch:{$gt: "america"}}, "_id": 0}
    )
    

    更新文档(up)

    我们之前介绍了更新文档,为什么还要介绍呢?因为文档的更新内容比较多,上面是先介绍了一部分。

    如果我想更新一个数组里面的元素怎么办呢?

    比如我们把boy1的工资由8000改成8001

    db.boys.updateOne({"name": "boy1"}, {$set:{"info.1": 8001}})
    我们要更新的是info里面的第二个元素,所以直接info.1即可
    

    现在info数组里面有四个元素,因此最大索引是3,这里我们指定了4,会有什么后果
    db.boys.updateOne({"name": "boy2"}, {$set:{"info.4": "yoyoyo"}})
    

    我们看到自动追加了,那如果这里指定的不是info.4,而是info.5呢?那么索引为5的地方依旧是yoyoyo字符串,索引为4的地方则是null。因此向数组字段范围外的位置添加新值,那么数组字段的长度会扩大,未被赋值的成员将被设置为null

    unset

    set表示设置值,还有一个unset表示删除值。

    我们把name="boy2"的info中索引为4的"yoyoyo"给删掉
    db.boys.updateOne({"name": "boy2"}, {$unset: {"info.4": ""}})
    

    我们发现unset对于数组来说,并不是真正的删除,而是把它变为null

    但是对于整个field来说,则是真的全部都删除了,并且我们似乎还给info指定了一个字符串,这个不影响,不管你指定什么都会删除,所以我们一般都写""

    另外,如果使用unset的时候,指定的字段不存在,那么不会有任何影响。

    rename

    rename是字段重命名,如果字段存在,那么重命名,字段不存在,则不会有任何动作。

    rename的原理就是unset旧字段,然后set新字段,所以我们发现它跑到后面去了。

    另外如果是嵌套字段怎么办呢?

    我们来做如下动作
    db.girls.updateOne(
    	{"姓名": "satori"},
    	{$rename: {"info.sister": "sister", "姓名": "info.姓名"}}
    )
    

    我们发现即便是嵌套字段,依旧是老规矩,info.字段的方式,我们把"info.sister"这个字段捞出来,变成了sister,把姓名这个字段捞出来变成了"info.姓名",两个字段就进行对调了。如果是数组里面的文档,是不行的,假设这里有一个名为array的数组,数组的第一个元素是个文档,文档有一个"hobby"字段,那么rename就是array.0.hobby,但是很遗憾这样不行,因为MongoDB不允许对数组里面的文档的元素进行rename,不能把外部的字段rename到数组里面的文档的某个元素,也不能把数组里面的文档的元素rename到外面。

    inc

    对字段增加指定的数值,可以为正、可以为负。

    指定字段不存在,那么会创建一个新字段,而值就是0 + 指定的值,也就是原来指定要增加的值

    mul

    和inc类似,只不过inc是相加,mul是相乘。不再演示,但注意的是inc和mul只能应用于数据类型为整型的字段

    指定字段不存在,那么会创建一个新字段,而值就是0 * 指定的值,也就是0

    min、max

    如果原来的age小于18,那么还是原来的值,大于18,那么改成18
    db.boys.updateOne(
    	{"sister": "古明地觉"},
    	{$min: {"age": 18}}
    )
    
    如果原来的age大于18,那么还是原来的值,小于18,那么改成18
    db.boys.updateOne(
    	{"sister": "古明地觉"},
    	{$max: {"age": 18}}
    )
    

    如果指定字段不存在,那么会创建新字段,字段的值就是min或者max指定的值。但是我们提供的更新值不是整型、或者说和原来的数据类型不一致,会怎么办呢?这时候MongoDB会采用类型比较,在MongoDB中类型也是有排序的

    null < numbers(ints,longs,doubles,decimals) < Symbol,String < Object < Array < Bindata < ObjectId < Boolean < Date < TimeStamp < Regular Expression

    更新数组

    先插入一条记录
    db.girls.insertOne(
    	{"name": "hanser", "info":["sing", ["old_driver", "baby", "little_angle"], "cast"]}
    )
    

    addToSet: 添加元素进入数组

    如果想插入多个元素呢?

    db.girls.updateOne(
    	{"name": "hanser"}, 
    	{$addToSet: {"info": ["up主", "卖奶粉"]}}
    )
    

    我们发现一个比较申请的地方是,它把这个列表作为一个整体插入进去了,这是MongoDB的策略,但是我们就像当成两个元素插入呢?可以使用each

    db.girls.updateOne(
    	{"name": "hanser"}, 
    	{
    		$addToSet: {"info": {$each: ["up主", "卖奶粉"]}}
    	}
    )
    

    pop:删除数组的第一个或者最后一个元素

    删除最后一个元素,就是-1。如果是删除数组里面的数组的某个元素,那么调用info.索引的方式

    当pop掉最后一个元素时,留下空数组,并且pop只能用于数组上。

    pull:删除数组的指定元素

    删除以s开头的元素
    db.girls.updateOne(
    	{"name": "hanser"},
    	{
    		$pull: {"info": /^s/}
    	}
    )
    

    我们注意到数组里面还有两个数组,我们下面删除里面出现包含up两个字的元素的数组
    我们使用了elemMatch,这个只会对里面的数组进行操作
    db.girls.updateOne(
    	{"name": "hanser"},
    	{
    		$pull: {
            	"info": {$elemMatch: {$regex: /^up/}}
    		}
    	}
    )
    

    此时元素含有up的数组就被我们删掉了。

    push:向数组添加元素

    不管元素是否在array里面,直接添加

    $:比较抽象,举例说明

    我们看到info是一个数组,让一个数组等于一个字符串,要是通过find肯定是查找不到的
    这里是为了更新做准备的。
    db.girls.updateOne(
    	{"name": "hanser", "info": "up主"},
    	{
    		$set: {
            	"info.$": "陈阿姨"
    		}
    	}
    )
    
    上面的操作就会将info里面值为"up主"的元素改成"陈阿姨"
    

    同理,如果改数组里面的数组的值呢?
    这个一般是改所有值,$[]更新所有的元素
    db.girls.updateOne(
    	{"name": "hanser"},
    	{
    		$set: {
            	"info.0.$[]": "陈阿姨"
    		}
    	}
    )
    

    python连接MongoDB

    python连接MongoDB,使用一个模块叫做pymongo,直接pip install pymongo即可

    import pymongo
    client = pymongo.MongoClient(host="localhost",port=27017)
    
    # 指定数据库
    db = client["test"]
    
    # 指定集合
    collection = db["girls"]
    
    # 插入数据,可以是一条、或者多条
    # 可以用insert_one和insert_many替代
    collection.insert({"name": "hanser"})
    collection.insert([{"name": "hanser"}, {"name": "yousa"}])
    
    # 查询
    collection.find_one()  # 返回单个
    collection.find()  # 返回多个
    
    
    # 更新
    collection.update_one()
    collection.update_many()
    
    # 删除
    collection.delete_one()
    collection.delete_many()
    

    api和Mongo shell基本一致,这里不再演示

    MongoDB之索引

    当数据量越来越大的时候,我们一般会建立索引。比如一个集合里面有100万条文档,我现在要找到所有name="satori"的文档,如果没有索引的话,那么只能全集合扫描,如果name="satori",那么就取出来,显然这个时间复杂度就高了。

    但如果我们给name字段加上了索引,那么MongoDB就会将name这个字段单独取出来进行排序(使用的数据结构为B-tree),排完序之后分别指向对应的文档,这样我在查找的时候是不是就快很多了呢?所以这是牺牲了点空间来换取时间的策略。

    我们这里只是根据name来进行筛选,那么如果有多个字段呢?假设还有country、address等多个字段,那么会将字段组合起来建立联合索引,进行排序。

    但是mongodb的联合索引有一个特点,假设我们后面可能会使用A、B、C这个三个字段进行查找,那么我们可以根据A、B、C这三个字段建立联合索引。虽然我们建了三个字段为联合索引,但如果只根据两个字段进行筛选呢?根据A,AB,ABC筛选,索引都能发挥作用,但是根据B,C,BC筛选不行,也就是必须以A开始,也就是联合索引的第一个字段开始,这是MongoDB对索引采取的特点。因此建立索引一定要根据集合里面数据或者文档的结构,否则的话,建立的就不是一个好索引。

    关于所以的更多特点我们下面介绍

    索引相关操作

    创建索引

    db.<collection>.createIndex({"name": 1})
    此时我们就创建了一个索引,里面1表示正向排序,-1表示逆向排序
    另外如果创建的索引只根据一个字段,那么这个索引称之为单建索引
    
    根据多个字段创建的索引叫做复合键索引
    还有多键索引,这个是针对数组的,会把数组中每一个元素,都会在多建索引中创建一个键
    
    另外集合中有一个默认的索引,是针对与_id的,这个是MongoDB自动创建的
    

    查看索引

    db.<collection>.getIndexes()
    

    测试索引效果

    db.<collection>.explain().find({...: ...})
    我们可以调用find函数,当然还可以调用其他的方法,比如sort等等
    只是说我们在前面加上了一个explain(),这样的话MongoDB就会打印出执行时候的具体细节
    

    删除索引

    在MongoDB中没有所谓的更新索引,如果要更新某些字段上已经创建的索引
    那么必须先删除原索引,然后重新创建索引。
    否则新索引不会包含原有文档
    
    
    db.<collection>.dropIndex(index_name)
    删除索引有两种方式,一种是传入索引名,这个通过getIndexes()返回的name字段可以查看
    
    还有一种方式是通过索引的定义删除
    db.<collection>.createIndex({"name": 1, "age": -1})我们定义了这个索引假设叫name_1_age_1
    那么删除索引的时候除了可以把这个索引名称传进去,还可以传入索引的定义
    db.<collection>.dropIndex({"name": 1, "age": -1})
    

    创建索引的第二个参数

    db.<collection>.createIndex({"name": 1}, {})
    这里面还有第二个参数,叫做options,来描述索引的特性的
    
    db.<collection>.createIndex({"name": 1}, {"unique": true})
    这就表示把name作为索引,但是还要求写入集合的所有文档的name都是不同的。
    
    如果已经存在了相同的name呢?那么MongoDB就不会允许在name创建唯一性索引,会失败
    
    如果我们新增的文档,里面没有name,那么MongoDB会默认添加一个name,并且值为null,但只能写入一次
    如果再来一篇没有name的文档,那么MongoDB依旧会默认添加一个name,并且值为null,那么此时就重复了
    
    此外还可以设置索引的过期时间
    db.<collection>.createIndex({"name": 1}, {"expireAfterSeconds": 20})
    那么20s之后,这个索引就过期了
    

    MongoDB之聚合

    先来看看MongoDB支持哪些聚合函数。

    • $sum:计算总和
    • $avg:计算平均值
    • $min:获取所有文档对应值的最小值
    • $max:获取所有文档对应值的最大值
    • $push:在结果文档中插入值到数组中
    • $addToSet:在结果文档中插入值到数组中,存在则不添加
    • $first:根据排序,获取文档的第一个数据
    • $last:根据排序,获取文档的最后一个数据

    下面我们演示一下sum

    创建数据集

    db.students.insertMany(
    	[
    		{"name": "satori", "age": 16, "score": 90},
    		{"name": "mashiro", "age": 16, "score": 86},
    		{"name": "kurisu", "age": 20, "score": 90},
    		{"name": "koishi", "age": 16, "score": 86},
    		{"name": "tomoyo", "age": 18, "score": 90},
    		{"name": "nagisa", "age": 20, "score": 90}
    	]
    )
    

    sum

    select age, count(score) as count_score from t group by age
    
    ==>
    
    db.students.aggregate(
    	[
        	{$group: {"_id": "$age", "count_score": {$sum: "$score"}}}
    	]
    )
    
    
    字段要加上$,聚合的字段必须起名为_id,
    "count_score": {$sum: "$score"},count_score相当于起了一个别名
    

    我们貌似没有看到count啊,sum可以实现count的效果

    db.students.aggregate(
    	[
        	{$group: {"_id": "$age", "count_score": {$sum: 1}}}
    	]
    )
    
    $sum不指定字段,指定1即可,会计算每个相同的age出现的次数
    
    db.students.aggregate(
    	[
        	{$group: {"_id": null, "count_score": {$sum: 1}}}
    	]
    )
    _id指定为null,表示不根据某个字段聚合,那么求出的就是整个集合的文档数量
    

    如果根据两个字段聚合怎么办?难道都叫"_id"?

    db.students.aggregate(
    	[
        	{$group: {"_id": {"age": "$age", "score": "$score"}, "count_name": {$sum: 1}}}
    	]
    )
    

    这就是MongoDB,个人觉得设计的真是奇葩。

    project

    返回的时候不想返回_id

    db.students.aggregate(
    	[
        	{$group: {"_id": {"age": "$age", "score": "$score"}, "count_name": {$sum: 1}}},
        	{$project: {"_id": 0}}
    	]
    )
    

    match

    选出score > 86的,然后进行聚合
    这个相当于mysql中的where是在where条件之后再进行聚合
    
    db.students.aggregate(
    	[
    	    {$match: {"score": {$gt: 86}}},
        	{$group: {"_id": {"age": "$age", "score": "$score"}, "count_name": {$sum: 1}}},
    	]
    )
    

    如果想返回count_name大于1的,怎么办呢?

    db.students.aggregate(
    	[
    	    {$match: {"score": {$gt: 86}}},
        	{$group: {"_id": {"age": "$age", "score": "$score"}, "count_name": {$sum: 1}}},
        	{$match: {"count_name": {$gt: 1}}}
    	]
    )
    在group底下再加上一个match即可,这个相当于mysql的having
    这个aggregate函数类似于一个管道的,接收一个数组,依次执行数组里面的每一个操作
    

    sort、skip、limit

    db.students.aggregate(
    	[
        	{$group: {"_id": {"age": "$age", "score": "$score"}, "count_name": {$sum: 1}}},
        	{$sort: {"count_name": 1}},
        	{$skip: 1},
        	{$limit: 2},
        	{$project: {"_id": 0}}
    	]
    )
    


    其余的可以自己尝试

  • 相关阅读:
    ldap和phpldapadmin的安装部署
    Django Model基础操作
    vmware_vcenter_api
    salt-api使用
    【如何设置博客园好看的标题样式】
    【我的python之路】
    8.20 总结
    抽象类和接口
    java 值传递 数组传递
    JAVA 构造函数 静态变量
  • 原文地址:https://www.cnblogs.com/traditional/p/12152615.html
Copyright © 2011-2022 走看看