1. 前言
在上一篇文章中,我们介绍了MongoDB。现在,我们来看下如何在MongoDB中进行常规的CRUD操作。毕竟,作为一个存储系统,它的基本功能就是对数据进行增删改查操作。
MongoDB中的增删改查操作,不同于我们熟悉的关系数据库中的操作。在关系数据库中,比如MySQL,我们通常使用SQL语句对数据库进行增(INSERT)删(DELETE)改(UPDATE)查(SELECT)。MongoDB在对数据进行操作过程中,使用的是Document进行数据操作。在对数据库进行操作的时候,使用Document来表示需要查询的条件和需要更新的数据,功能类似于关系数据库中的SQL语句。接下来,我们来看下在MongoDB中是如何通过Document来进行CRUD操作的。
注意:下面介绍的CRUD操作,是在MongoDB的mongo shell操作的。mongo shell是一个JavaScript交互环境,是MongoDB提供的一个客户端。不同的语言有自己对应的驱动和对应的操作API,但是原理是类似的。相信通过在mongo shell中进行CRUD操作的介绍,大家也可以举一反三,在不同的语言中操作MongoDB。
2. INSERT操作
MongoDB中的新增操作,把一个新的Document插入到一个Collection中。
如果该Collection不存在,则新增一个新的Collection。这个和关系数据库有很大的区别,在关系数据库中,我们需要定义数据库的schema和表结构,这是NoSQL(MongoDB是NoSQL的一种)数据库和关系数据库很大的区别。在MongoDB中,我们可以在一个Collection中包含多个不同结构的Document(不推荐这样做,一个Collection最好具有相同格式的Document,便于维护和使用)。
关于"_id"字段,当我们新增一个Document到Collection中的时候,MongoDB需要每一个新增的Document中有一个"_id"字段,MongoDB把这个字段作为主键,所以要求这个字段在Collection中是唯一的。如果新增的Document中没有包含"_id"字段,那么MongoDB的客户端会在该Document中新增一个值为ObjectId类型的"_id" 字段;如果MongoDB服务在新增Document的时候发现Document中没有"_id"字段,那么mongod会新增一个值为ObjectId类型的"_id"字段到该Document中。
MongoDB对单个Document的写操作是原子的。
MongoDB提供了如下的方式来进行新增操作:
- db.collection.insert()
- db.collection.insertOne() 3.2版本新增
- db.collection.insertMany() 3.2版本新增
2.1 db.collection.insert()
insert操作可以新增单个Document,也可以新增多个Document。如果新增单个Document,则把需要新增的Document作为参数传递给insert(),如果新增多个Document,则将多个Document的数组作为参数传递个insert()函数。
比如我们需要在"post"这个collection上新增一个Document来表示我们的博客中新增了一篇文章,我们可以这么做:
现在,我们给post这个Collection新增了一个Document,表示在Blog中新增了一篇文章。我们可以看下现在post这个Collection中是不是有我们新增的Document。
这里我们使用findOne()来查询,我们可以看到,我们由于没有在Document中包含"_id"字段,所以MongoDB自动为我们新增了一个"_id"字段。
insert()函数可以一次新增多个Document,只要将一个Document的数组传递给insert()就可以了,如:
2.2 db.collection.insertOne()
insertOne()函数是在3.2版本中新增的,它用来添加单个Document。例子如下:
2.3 db.collection.insertMany()
insertMany()函数是insert的批量增加的版本,支持一次新增多个Document,它也是3.2版本中新增的函数:
好了,我们简单介绍了下insert操作的三个函数,下面,我们已经在MongoDB的数据库里新增了几个Document了。接下来,是时候开始学习查找操作来查看这些已经存储在MongoDB的记录了。
3. QUERY操作
MongoDB提供了db.collection.find()函数来执行查询操作,函数将返回一个游标(cursor),用于遍历查询到的Documents。find()函数接受两个参数,一个是过滤条件,还有一个是投影。
db.collection.find( <query filter>, <projection> )
- <query filter>用于查找满足过滤条件的Document
- <projection>(投影)用于指定被找到的Document中需要返回哪些字段,用于限制网络中传输的数据的大小。投影的概念和关系数据库中的投影的概念基本是一致的。
现在,我们可以查询下刚才我们新增的所有的Documents,通过find(),我们来看下如何查询:
如果我们没有传递任何参数,或者传递一个"{}"给find()函数,那会find()返回Collection中所有的Document。现在,可以看到我们刚才新增的所有的Documents。
接下来,我们通过指定条件,查询标题为"Third Post"的Document,我们可以这样做:
发现了么,我们的查询条件其实就是以Document方式构造的。在MongoDB中,我们可以通过构造不同的Document来定义不同的查询条件。
除了使用具体的字段来过滤,我们还可以使用查询操作符来做更加灵活的操作。我们现在需要查找出标题是"Third Post"或"Fifth Post"这两篇Post中的任何一篇,我们可以使用$in操作符来查询:
除了使用$in,MongoDB还提供了很多有用的操作符来帮助构造过滤条件,如 $lt, $gt, $and, $or等。
3.1 使用子Document中值作为过滤条件
现在,我们假设有一篇文章,在Document中的存储如下:
现在,我们需要匹配内部Document中的条件,比如我们需要查找author中name是Duke的记录,我们可以这样构造我们的筛选条件:
db.post.findOne( { "author": { "name": "Duke", "email": "[email protected]" } })
查询结果就是这样的
我们观察这个过滤条件,发现和上面的过滤条件的结构一样,就是{"field":"value"}的格式,只是不同的是,这次的"value"不再是一个普通的值了,而是一个Document。通过这种方式来过滤,具有一些局限性,我们如果要查找author中name是Duke的记录,我们必须完整指定author的值,也就是需要完整的Document。如果需要只过滤name,而不关系email的值,我们可以这样构造过滤条件:
db.post.findOne({"author.name": "Duke"})
查询到和上面一样的结果
但是,这次我们使用了"author.name"的方式来指定field。这种类似于成员引用的"点符号"结构的查询,可以用来对内嵌的Document中的指定的field进行过滤。后面你将会看到,对于数组类型的值,也可以通过类似的方式来构造过滤条件。
3.2 使用数组中的元素作为过滤条件
接下来我们看下如何使用数组中的值来构造过滤条件。最简单的,也是最容易想到的,就是把整个数组作为过滤条件,类似于上面的把整个子Document作为过滤条件一样,我们可以这样构造
db.post.findOne({"comments": ["comment one", "comment two"]})
查询的结果如下:
但是,这种方式没法指定数组中的某个值作为过滤条件。如果要使用数组中的某个值作为过滤条件,我们可以这么构造过滤条件:
db.post.find({"comments": "comment two"})
查询到的结果如下:
这里最后使用了"pretty()"方法来格式化输出,输出格式化的数据,便于观察,仅此而已,没有别的用途。我们可以看到,这种方式构造的过滤条件,使用了数组中的一个元素来筛选记录,只要数组中包含了这个元素(不管这个元素的下标),那么就会过滤出这条记录,有点像集合中的in操作。这种方式可以匹配数组中的一个元素。
接下来,我们更近一步,我们需要匹配数组的某一个下标位置的元素,那么我们需要使用上面一开始提到的,使用类似匹配子Document的那种成员引用(点符号)方式来构造过滤条件:
db.post.find({"comments.0": "comment two"})
查询到的结果如下:
这里,我们使用{"comments.0": "comment two"}的方式指定匹配的条件是:数组"comments"下标为0的位置的值为"comment two"。这样,就可以过滤掉之前下标为1的位置为"comment two"的记录了。可以看出,在使用数组下标构造过滤条件的时候,下标是从0开始的。
MongoDB提供了丰富的操作符来支持构造灵活的过滤条件,这里就先介绍这么点。由于篇幅关系,这里就不再展开了,下次独立写篇文章重点介绍下MongoDB中的查询操作。接下来,我们该看下如何在MongoDB中进行更新操作。
4. UPDATE操作
MongoDB也支持基本的更新操作,它提供了4个用于更新的方法:
- db.collection.updateOne() 3.2版本新增
- db.collection.updateMany() 3.2版本新增
- db.collection.update()
- db.collection.replaceOne() 3.2版本新增
这些Update方法支持三个参数:
- 用于筛选记录的过滤条件,过滤出需要被更新的记录,过滤条件和query中使用的过滤条件类似。
- 一个新的Document,用于更新部分值或者替换除了"_id"之外的一整个Document。
- 一个以Document格式组织的一组更新选项。
MongoDB对于单个Document的更新操作是原子的。 MongoDB在更新时对于"_id"主键的处理原则是,不管是更新还是替换Document,都不能更改被更新的Document的"_id"主键,如果在替换的时候包含了不同的"_id",那么替换会失败,如:
上面的例子中,我们修改了原先的"_id"值为1,然后进行替换更新,发现更新失败,提示"_id"值不能被更改。
当我们更新的时候,新的Document的大小超过了原先旧的Document的大小的时候,更新操作会重新申请一块更大的空间来存放这个新的Document。
接下来,我们来看下这些更新API的用法。
4.1 db.collection.updateOne()
updateOne()函数是在3.2版本中新增的一个API,用于更新一条匹配到的Document。现在我们需要更新我们的Post集合中的标题为"First Post"的Document,我们想增加一些评论,我们可以这么构造我们的更新表达式:
db.post.updateOne({"title": "First Post"}, {"$set": {"comments": ["comment one"]}})
现在,我们的"First Post"这篇文章就有了一条评论了。这里我们使用"$set"操作符来设置一个新的字段,MongoDB的更新操作提供了一些有用的操作符来帮助构造更新语句。updateOne()函数会更新第一个被匹配到的Document。如果要更新多个Document,我们可以用updateMany()函数来支持。
4.2 db.collection.updateMany()
updateMany()函数可以对匹配到的所有的Document进行更新操作。比如我们想更新所有的文章,让每篇文章都有一个浏览数的字段,表示别浏览的次数。我们可以这样做:
db.post.updateMany({}, {"$set": {"view": 0}})
我们可以看到,通过updateMany()函数,我们更新了所有的Document,使得每个Document都包含了一个"view"字段,初始值为0。
4.3 替换Document
上面提到了两个API,都是对Document中的某个字段进行更新。它们除了可以对Document中的单个字段进行更新外,还可以对匹配到的整个Document进行替换(除了"_id"属性外,可以替换任何属性)。只要把更新参数改成一个普通的Document(Document的结构中不包含操作符),就可以对匹配到的Document替换成参数中的Document。进行Document替换更新的时候,需要注意:原先的Document中的"_id"属性是不能被更改的,所以新的用于替换的Document不能包含"_id"属性,如果包含了"_id"属性,那么这个"_id"属性必须是和被更新的Document的"_id"属性是相同的。
除了上面的两个API可以用于替换Document,MongoDB在3.2版本中新增了一个replaceOne()函数来进行替换操作。现在我们用replaceOne()来替换一个Document,我们把"title"是"First Post"的Document替换成新的Document:
db.post.replaceOne({"title": "First Post"}, {"title": "New Post", "content": "new content", "create_time": new Date()})
现在,我们已经把"title"为"First Post"的Document替换为了新的"title"为"New Post"的Document了。
最后,我们来看下上面三个API的集大成者,就是update()函数,它基本包含了上面提到的三个API的所有功能,默认情况下,update()函数会更新匹配到的第一个Document,如果设置了"multi"选项,那么就可以更新匹配到的所有的Document。
下面我们使用update()来更新匹配到的第一个Document:
db.post.update({"title": "New Post"}, {"$set": {"view": 0}})
好了,更新操作介绍到这里,接下来就是最后一个删除操作了。
5. DELETE操作
到这里,我想大家都已经了解了MongoDB中的"增","改","查"的功能了,接下来我们来看下"删"这个功能。MongoDB提供了三个用于删除操作的API,分别是:
- db.collection.deleteOne()
- db.collection.deleteMany() 3.2版本新增
- db.collection.remove() 3.2版本新增
这三个API都支持一个过滤条件参数,用于匹配到满足条件的Document,然后进行删除操作。
从三个API的字面意思我们可以看出,deleteOne()会删除匹配到的所有的Document中的第一个,而deleteMany()和remove()会删除所有匹配到的Document。
假设我们需要删除"title"为"New Post"的Document,我们可以用deleteOne()来操作
db.post.deleteOne({"title": "New Post"})
当删除了"title"为"New Post"的Document以后,我们再次去查询的时候,发现这个Document确实已经被删除了。deleteOne()用于删除单个Document,如果我们需要删除所有满足过滤条件的Document的话,我们可以用deleteMany()或者remove()来实现。
现在我们想删除所有浏览数为0的文章,那么我们可以用deleteMany()或者remove来实现:
db.post.remove({"view": 0})
由于我们存储在集合中的所有Document的view值都是0,所以上面的操作相当于我们清空了我们的Collection。
6. 总结
好了,到这里,我们已经简单介绍了MongoDB中相关的CRUD操作了。相信大家对MongoDB的基本操作有了一些简单的认识了。
差不多就先写到这里了,由于文章的主题就是跟大家介绍MongoDB的CRUD操作,所以上面很多的细节部分没有展开,感兴趣的同学可以去翻阅MongoDB的文档来了解,有机会,下次单独拎出来好好总结下。毕竟,我们学东西,得先有个大概的了解以后,才可以有针对的深入,一味的追求细节,往往会迷失方向。