对于我们这种菜鸟来说,最重要的不是数据库的管理,也不是数据库的性能,更不是数据库的扩展,而是怎么用好这款数据库,也就是一个数据库提供的最核心的功能,增删查改。 因为mongodb存储数据都是以文档的模式,所以在操作它的数据时,也是以文档为单位的。
对于我们这种菜鸟来说,最重要的不是数据库的管理,也不是数据库的性能,更不是数据库的扩展,而是怎么用好这款数据库,也就是一个数据库提供的最核心的功能,增删查改。
因为mongodb存储数据都是以文档的模式,所以在操作它的数据时,也是以文档为单位的。那么我们实现增删查改也是以文档为基础,不知道文档是什么的同学可以看看上篇介绍的基本概念。
1.插入文档
向mongodb集合中插入文档的基本方法是insert:
单个插入> document = {key : value}> db.collectionname.insert(document)批量插入> documents = [{key : value}, {key : value}, {key : value}, ...]传入数组> db.collectionname.insert(documents)for循环> for(var i = 0; i db.people.find(){ _id : 1, name : mary, age : 20 }> db.people.insert({_id : 1, name : amy, age : 22})e11000 duplicate key error index: test.people.$_id_ dup key: { : 1.0 }> db.people.save({_id : 1, name : amy, age : 22})> db.people.find(){ _id : 1, name : amy, age : 22 }
本来people集合中已经有一个文档了,如果我们要插入的文档中的主键_id在集合中已经存在一样的值则会报错,而save就不会。没有一样的主键时,insert和save一样都执行插入操作,有一样的主键时,insert就报错,save就执行更新操作,用新文档的内容来替换已经原来的内容,和update操作一样,所以mary被替换成了amy。
执行插入操作的时候,驱动程序先将数据转换成bson的形式,然后再送入数据库,只检验是否包含了_id键,文档是否超过4mb(能插入的文档最大为4mb,超过了就不能存入数据库),而不进行别的验证和执行别的代码,所以远离了注入式攻击,使数据库更安全。
2.删除文档
删除文档的基本方法是remove:
db.collectionname.remove()db.collectionname.remove({})db.collectionname.remove(condition)db.collectionname.drop()
前三种都是删除集合中的文档,第一个和第二个是一样的,都不指定删除的条件,这样会删除掉集合中所有的文档,第四个是删除该集合,连索引都删除了,如果要清除整个集合,直接删除集合的效率比删除集合中的所有文档更快。第三个指定了一个条件,比如删除people集合中name为mary的所有文档:
db.people.remove({name : mary})
3.更新文档
更新文档的基本方法是update,虽然上面讲到了save在_id一样的时候也有这种功效,当然我们一般是不会以_id为更新条件的:
db.collectionname.uodate(condition, modifier, upsert, multi)
update有四个参数,condition是查询文档,用来查询出要修改的文档,modifier是修改器文档,描述对查找到的文档做哪些修改,upsert表示如果没有该文档就插入这个心文档,默认为false,multi表示是否更新查询到的所有文档,默认为false,所以只更新查询到的第一个文档。但是我们平时经常用到的还是前两个参数,如果有必要就用后两个参数。因为更新操作是原子的,所以当有很多更新同时发生时,最后一次的更新操作才是最后的结果。下面我们将name为mary的文档age改为20:
> db.people.find(){ _id : objectid(53a3a0b7abda49d7dfce102c), name : mary, age : 30, country : us }> db.people.update({name : mary}, {age : 20})> db.people.find(){ _id : objectid(53a3a0b7abda49d7dfce102c), age : 20 }
但是最后的结果却出乎我们的预料,name字段和country字段都没有了,这样的update是用后面的文档替换查询到的文档,所以结果是正确的,只是我们的操作错误了。那我们每次更新不都要将原来的数据再写一次么,这样比写sql更复杂啊,不过还有一种改进方式,在shell中将查询到的文档赋值给一个变量,然后再修改,最后在将这个变量替换原来的查询结果:
方式一:> db.people.find(){ _id : objectid(53a3a1c1abda49d7dfce102d), name : mary, age : 30, country : us }> db.people.update({name : mary}, {name : mary, age : 20})> db.people.find(){ _id : objectid(53a3a1c1abda49d7dfce102d), name : mary, age : 20 }方式二:> db.people.find(){ _id : objectid(53a3a1c1abda49d7dfce102d), name : mary, age : 30, country : us }> person = db.people.findone(){ _id : objectid(53a3a1c1abda49d7dfce102d), name : mary, age : 30, country : us}> person.age = 20> delete person.countrytrue> db.people.update({name : mary}, person)> db.people.find(){ _id : objectid(53a3a1c1abda49d7dfce102d), name : mary, age : 20 }
查询出name为mary的文档赋值给person,然后将person的age赋为20,删除country字段,再替换原来查询到的文档,如果一个文档有很多字段,而我们只要修改其中一到两个字段,这种方式就比第一种方式要简单的很多。但是这样就有个问题,如果findone匹配到第二个mary,更新的时候却更新到了第一个mary,那就会出现_id重复的问题。这样就有了修改器的使用,是更新变的更简单高效。
1).$set
$set用来修改指定键的值(也可以是内嵌文档的键),如果这个键不存在就创建它,如:
> db.people.find(){ _id : objectid(53a3b0e633c516902a43a790), name : mary, age : 30, country : us, school : { name : beijinguniversity, city : beijing } }> db.people.update({name : mary}, {$set : {age : 20, school.name : qinghuauniversity}})> db.people.find(){ _id : objectid(53a3b0e633c516902a43a790), name : mary, age : 20, country : us, school : { name : qinghuauniversity, city : beijing } }
2).$unset
$unset用来将键完全删除,如:
> db.people.find(){ _id : objectid(53a3a1c1abda49d7dfce102d), name : mary, age : 20, country : us }> db.people.update({name : mary}, {$unset : {country : 1}})> db.people.find(){ _id : objectid(53a3a1c1abda49d7dfce102d), age : 20, name : mary }
3).$inc
$inc用来增加已有键的值,当键不存在时创建这个键,再在初始值0的基础上再增加,$inc对应的键必须是数字类型的,如果是别的数据类型就会报错,如:
> db.people.find(){ _id : objectid(53a3b0e633c516902a43a790), age : 20, country : us, name : mary }> db.people.update({name : mary}, {$inc : {age : 1}})> db.people.find(){ _id : objectid(53a3b0e633c516902a43a790), age : 21, country : us, name : mary }> db.people.update({name : mary}, {$inc : {age : -2}})> db.people.find(){ _id : objectid(53a3b0e633c516902a43a790), age : 19, country : us, name : mary }> db.people.update({name : mary}, {$inc : {country : 2}})cannot apply $inc modifier to non-number
4).$push,$pushall
$push用来对内置数组的操作,给内置数组添加元素,$push是添加一个元素,$pushall是添加一个数组,如:
> db.people.find(){ _id : objectid(53a3b47733c516902a43a791), name : mary, friends : [ amy, join ] }> db.people.update({name : mary}, {$push : {friends : joe}})> db.people.find(){ _id : objectid(53a3b47733c516902a43a791), friends : [ amy, join, joe ], name : mary }> db.people.update({name : mary}, {$push : {friends : [jodan, mical]}})> db.people.find(){ _id : objectid(53a3b47733c516902a43a791), friends : [ amy, join, joe, [ jodan, mical ] ], name : mary }
5).$addtoset,$each
$addtoset用来给内置数组添加元素,如果数组中已经存在相同值的元素,就不添加,用来避免重复,通常和$each组合使用,$each后面跟一个数组用来添加该数组中的元素如:
> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ amy ], name : mary }> db.people.update({name : mary}, {$addtoset : {friends : join}})> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ amy, join ], name : mary }> db.people.update({name : mary}, {$addtoset : {friends : {$each : [amy, join, jodan]}}})> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ amy, join, jodan ], name : mary }
6).$pull,$pullall
$pull用来删除内置数组中的某元素,$pullall删除某几个元素,如:
db.people.find(){ _id : objectid(53a3c18533c516902a43a794), name : mary, friends : [ amy, join, jodan, mical ] }> db.people.update({name : mary}, {$pull : {friends : amy}})> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ join, jodan, mical ], name : mary }> db.people.update({name : mary}, {$pullall : {friends : [join, jodan]}})> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ mical ], name : mary }
7).$pop
$pop也是用来删除内置数组中的一个元素,这个元素要么是数组的第一个元素要么是数组的最后一个元素,如:
> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ mical, amy, join ], name : mary }> db.people.update({name : mary}, {$pop : {friends : 1}})> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ mical, amy ], name : mary }> db.people.update({name : mary}, {$pop : {friends : -1}})> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ amy ], name : mary }
8).$
$用来定位内置数组的元素,mongodb的内置数组下标也是从0开始递增的,所以我们可以通过对数组进行定位修改,如:
> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ amy, join, jodan ], name : mary }> db.people.update({name : mary}, {$set : {friends.0 : mical}})> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ mical, join, jodan ], name : mary }> db.people.update({name : mary, friends : join}, {$set : {friends.$ : amy}})> db.people.find(){ _id : objectid(53a3c18533c516902a43a794), friends : [ mical, amy, jodan ], name : mary }
上面第一种更新,我们就是单纯的想把mary的第一个朋友变成mical。第二种更新,我们不知道mary的朋友join是在数组的哪个位置,但是确实有这么个朋友,就查询到了通过$进行定位,然后再将其变成amy。
9).$rename
$rename用来对字段重命名,如:
> db.people.find(){ _id : objectid(53a3c80a33c516902a43a795), name : mary, friends : [ mical, amy, jodan ] }> db.people.update({name : mary}, {$rename : {friends : classmates}})> db.people.find(){ _id : objectid(53a3c80a33c516902a43a795), classmates : [ mical, amy, jodan ], name : mary }
文档的插入,删除和更新都是瞬间完成的,因为客户端并不需要等待数据库的响应,这个也不是异步操作,因为客户端将文档给数据库了其他的操作都由数据库本身来操作,和客户端就完全没关系了。文档的查询就不一样了,客户端必须要等到数据库返回来的查询结果。
4.查询文档
查询文档的基本方法是find:
db.collectionname.find(condition, returnkey)db.collectionname.find()db.collectionname.find({})db.collectionname.find({}, {name : 1})db.collectionname.find({}, {name : 0})
find有两个参数,condition为查询文档,returnkey为指定返回的字段。不指定查询条件或者查询条件是一个空文档,那就是查询集合中的所有文档,返回值指定键,如果为1就是返回该键的值,如果为0就返回除了该键以外的所有键的值。
当然我们除了精确查询,比如查询name为mary,age为20的文档并返回其friends字段的结果,还会有更复杂的模糊查询条件,比如范围查询,age在20到30之间的文档等。这时候在查询文档中就要添加这些查询条件。
1).$lt,$lte,$gt,$gte,$ne
$lt,$lte,$gt,$gte分别对应=,可以组合起使用来查询一个范围,$ne表示不等于,如:
> db.people.find(){ _id : objectid(53a3e01d33c516902a43a797), name : mary, age : 20 }{ _id : objectid(53a3e02533c516902a43a798), name : amy, age : 23 }{ _id : objectid(53a3e02d33c516902a43a799), name : join, age : 18 }{ _id : objectid(53a3e03c33c516902a43a79a), name : jodan, age : 25 }{ _id : objectid(53a3e04933c516902a43a79b), name : mical, age : 19 }> db.people.find({age : {$gte : 19, $lte : 23}, name : {$ne : mary}}, {_id : 0}){ name : amy, age : 23 }{ name : mical, age : 19 }
2).$or,$in,$nin
$or,$in表示几个条件中满足一个就可以返回,但是$in只能用于在同一个字段中给几个条件,而or可以用于不同字段,还能和$in搭配使用,$nin和$in相反,表示只要不符合几个给定的条件就可以返回,如:
> db.people.find(){ _id : objectid(53a3e01d33c516902a43a797), name : mary, age : 20 }{ _id : objectid(53a3e02533c516902a43a798), name : amy, age : 23 }{ _id : objectid(53a3e02d33c516902a43a799), name : join, age : 18 }{ _id : objectid(53a3e03c33c516902a43a79a), name : jodan, age : 25 }{ _id : objectid(53a3e04933c516902a43a79b), name : mical, age : 19 }> db.people.find({age : {$in : [19, 20, 21, 22, 23]}}, {_id : 0}){ name : mary, age : 20 }{ name : amy, age : 23 }{ name : mical, age : 19 }> db.people.find({age : {$nin : [19, 20, 21, 22, 23]}}, {_id : 0}){ name : join, age : 18 }{ name : jodan, age : 25 }> db.people.find({$or : [{age : {$in : [19, 20, 21, 22, 23]}}, {name : jodan}]}, {_id : 0}){ name : mary, age : 20 }{ name : amy, age : 23 }{ name : jodan, age : 25 }{ name : mical, age : 19 }
3).$not,$mod
$not是元条件句,可以用在任何其他条件上,表示非,$mod表示取模运算,对应运算符%,如:
> db.people.find(){ _id : objectid(53a3e01d33c516902a43a797), name : mary, age : 20 }{ _id : objectid(53a3e02533c516902a43a798), name : amy, age : 23 }{ _id : objectid(53a3e02d33c516902a43a799), name : join, age : 18 }{ _id : objectid(53a3e03c33c516902a43a79a), name : jodan, age : 25 }{ _id : objectid(53a3e04933c516902a43a79b), name : mical, age : 19 }> db.people.find({age : {$not : {$mod : [5, 0]}}}, {_id : 0}){ name : amy, age : 23 }{ name : join, age : 18 }{ name : mical, age : 19 }
这里表示对age进行取模运算,如果里面的$mod条件为对5取模,为0就符合条件,加上外面的$not,表示非,也就是模不为0就符合条件。
4).$exists,null
$exists表示判断键值是否已经存在,它的值为true或者false,null表示空值或不存在的字段,如:
> db.variable.find(){ _id : objectid(53a3e91f33c516902a43a79c), x : 1, y : null }{ _id : objectid(53a3e92733c516902a43a79d), x : 3 }{ _id : objectid(53a3e92e33c516902a43a79e), x : 5, y : 10 }> db.variable.find({y : null}, {_id : 0}){ x : 1, y : null }{ x : 3 }> db.variable.find({y : {$in : [null], $exists : true}}, {_id : 0}){ x : 1, y : null }
当我们只查询y为null时,由于第二个文档中没有y字段所以也符合条件,那么如果要查询的文档y必须要存在而且为null,就用到了$exists。
5).正则表达式
没错,mongodb也支持正则表达式来查询字符串,这就使得对字符串的查询更加的简单高效,正则表达式是查询字符串的神器,如:
> db.people.find(){ _id : objectid(53a3e01d33c516902a43a797), name : mary, age : 20 }{ _id : objectid(53a3e02533c516902a43a798), name : amy, age : 23 }{ _id : objectid(53a3e02d33c516902a43a799), name : join, age : 18 }{ _id : objectid(53a3e03c33c516902a43a79a), name : jodan, age : 25 }{ _id : objectid(53a3e04933c516902a43a79b), name : mical, age : 19 }> db.people.find({name : /jo.*/}, {_id : 0}){ name : join, age : 18 }{ name : jodan, age : 25 }
6).对内置数组的查询,$all,$size,$slice
对内置数组最简单的查询就是精确的指定条件,指定的条件表示该内置数组包含该元素,而不是等于,这是很有意思的,精确查询一般的字段都是指该字段的值等于我们给定的值,而对内置数组而言,是表示包含该元素。
如果要对数组通过多元素来匹配就要用到$all,如果包含$all给定的数组中的元素,就符合条件判断,$size表示数组的大小,$slice是find中第二个参数中指定的内容,表示返回数组的一个子集合,也就是查询到的数组返回哪几个元素,正数表示前几个,负数表示后几个,如:
> db.food.find(){ _id : objectid(53a3ebcf33c516902a43a79f), fruit : [ apple, banana, orange ] }{ _id : objectid(53a3ee0e33c516902a43a7a0), fruit : [ bnana ] }{ _id : objectid(53a3ee1933c516902a43a7a1), fruit : [ apple, orange ] }> db.food.find({fruit : apple}, {_id : 0}){ fruit : [ apple, banana, orange ] }{ fruit : [ apple, orange ] }> db.food.find({fruit : {$all : [apple, orange]}}, {_id : 0}){ fruit : [ apple, banana, orange ] }{ fruit : [ apple, orange ] }> db.food.find({fruit : {$size : 2}}, {_id : 0}){ fruit : [ apple, orange ] }> db.food.find({fruit : {$size : 3}}, {_id : 0, fruit : {$slice : -2}}){ fruit : [ banana, orange ] }> db.food.find({fruit : {$size : 3}}, {_id : 0, fruit : {$slice : 2}}){ fruit : [ apple, banana ] }
7).对内置数组中文档的查询,$elemmatch
对内置数组中文档的查询其实就是将内置数组中的元素替换成文档形式,$elemmatch保证我们的查询是在内置数组中同一文档中进行的,如:
> db.grade.find(){ _id : objectid(53a3f4d933c516902a43a7a4), name : jim, age : 20, score : [ { course : computer, credit : 2, teacher : mary }, { course : english, credit : 1, teacher : join } ]}{ _id : objectid(53a3fbf7901e10bdd70adf9e), name : amy, age : 21, score : [ { course : computer, credit : 2, teacher : nola }, { course : chinese, credit : 3, teacher : mary } ]}方式一、这里指定score中course为computer和teacher为mary是查不到的> db.grade.find({score : {course : computer, teacher : mary}})方式二、指定score中元素的全部字段就能查到了,而且顺序不能变> db.grade.find({score : {course : computer, teacher : mary, credit : 2}})> db.grade.find({score : {course : computer, credit : 2, teacher : mary}}){ _id : objectid(53a3f4d933c516902a43a7a4), name : jim, age : 20, score : [ { course : computer, credit : 2, teacher : mary }, { course : english, credit : 1, teacher : join } ]}方式三、指定score.course为computer和score.teacher为mary居然把两个都查出来了> db.grade.find({score.course : computer, score.teacher : mary}){ _id : objectid(53a3f4d933c516902a43a7a4), name : jim, age : 20, score : [ { course : computer, credit : 2, teacher : mary }, { course : english, credit : 1, teacher : join } ]}{ _id : objectid(53a3fbf7901e10bdd70adf9e), name : amy, age : 21, score : [ { course : computer, credit : 2, teacher : nola }, { course : chinese, credit : 3, teacher : mary } ]}方式四、通过$elemmatch来查询,得到我们想要的结果> db.grade.find({score : {$elemmatch : {course : computer, teacher : mary}}}){ _id : objectid(53a3f4d933c516902a43a7a4), name : jim, age : 20, score : [ { course : computer, credit : 2, teacher : mary }, { course : english, credit : 1, teacher : join } ]}
对于方式一和方式二可能有个难理解的地方,为什么只指定其中部分字段就查询不到,而必须要指定全部的字段才可以呢,查询内置数组的时候只要指定fruit为apple就能查询出包含apple的数组,这里为什么要指定全部的字段呢。其实这是一个误区,在这里一个内置文档才相当于fruit中的一种水果,也就是数组中的一个元素,如果连这个元素都缺斤少两,在数组中如何查询的到呢。
对于方式三,指定score.course和score.teacher为啥把两个都查询出来了,明明第二个结果不是我们想要的,course为computer时teacher并不为mary,但是course为chinese时teacher为mary,所以第二个结果中的确存在score.course为computer,并且同时存在score.teacher为mary,那这是将两个文档的字段组合起来达到我们的要求了。显然这并不是我们想要的,所以就必须使用$elemmatc,也就是方式四,它保证了我们查询是在数组中同一个文档元素中进行的。
7).$where
$where可以执行任何的javascript作为查询的一部分,所以$where几乎可以做所有的事,但是这种方式必须要将每个文档从bson转换成javascript对象,然后再通过$where的表达式来执行,而且还不能用索引。因此用$where是下下策,在你实在走投无路的时候再用。用法也比较简单,就是对this的操作,如果某文档满足我们自定义的条件就返回true,那么该文档就会被返回,如:
> db.fruit.find(){ _id : objectid(53a412c7901e10bdd70adfa2), apple : 2, banana : 6, orange : 3 }{ _id : objectid(53a412d1901e10bdd70adfa3), apple : 5, banana : 7, orange : 8 }db.fruit.find({$where : function(){ for(var x in this){ for(var y in this){ if(this[x] / this[y] == 2){ return true; } } }}}){ _id : objectid(53a412c7901e10bdd70adfa2), apple : 2, banana : 6, orange : 3 }
数据库使用游标来返回find的执行结果,客户端对游标的实现通常能够对查询到的结果进行有效的控制,可以限制结果的数量,略过部分结果,根据任意方向,任意键的组合对结果进行排序,或者执行其他一些功能强大的操作。
在shell中使用游标,要先将查询到的结果赋值给一个变量,然后通过游标一次查看一条结果。如果查询结果没有赋值给变量,那么shell会自动迭代,打印所有查询到的结果。游标提供了两个方法,hasnext返回是否还有结果,next返回下一个结果用来遍历查询到的结果,还提供了limit,skip,sort几个方法来操作结果。因为游标对象的方法都返回游标本身,所以这几个方法可以以任意的顺序进行组合。如:
> db.people.find(){ _id : objectid(53a3e01d33c516902a43a797), name : mary, age : 20 }{ _id : objectid(53a3e02533c516902a43a798), name : amy, age : 23 }{ _id : objectid(53a3e02d33c516902a43a799), name : join, age : 18 }{ _id : objectid(53a3e03c33c516902a43a79a), name : jodan, age : 25 }{ _id : objectid(53a3e04933c516902a43a79b), name : mical, age : 19 }> var cursor = db.people.find();> while(cursor.hasnext()){ obj = cursor.next(); print(obj.name); }maryamyjoinjodanmical> var cursor = db.people.find().limit(3);> while(cursor.hasnext()){ obj = cursor.next(); print(obj.name); }maryamyjoin> var cursor = db.people.find().skip(2);> while(cursor.hasnext()){ obj = cursor.next(); print(obj.name); }joinjodanmical> var cursor = db.people.find().sort({name : 1});> while(cursor.hasnext()){ obj = cursor.next(); print(obj.name); }amyjodanjoinmarymical> var cursor = db.people.find().skip(2).limit(2).sort({name : 1});> while(cursor.hasnext()){ obj = cursor.next(); print(obj.name); }joinmary
limit表示对查询到的结果加上数量上限,如果查询到的结果数量多于这个上限,则返回这个数量的结果,如果不够,就返回全部的结果。skip表示略过一定数量的结果,如果查询到的结果数量少于这个数量,就没有任何文档返回,如果多于,则略过这个数量的文档后再返回。sort表示对查询结果进行排序,指定字段并指定顺序,1为升序,-1为降序,也可以同时对多个字段进行排序。