本文所有内容以MongoDB 3.2 为基础。
插入并保存文档
插入是添加数据的基本方法。可以使用insert
插入一个文档:
db.foo.insert({"bar": "baz"})
批量插入
使用批量插入,可以加快插入的速度。我们可以使用insertMany
来实现批量插入,它接收一个文档数组作为参数
db.foo.insertMany([{"id": 1}, {"id": 2}, {"id": 3}])
一次发送数十、数百乃至数千个文档会明显提高插入的速度。
本方法不能插入多个文档到多个集合中。只能插入多个文档到一个集合中。
但是一次性接受的最大消息长度是有限制的。每次接受的文档数组长度为1000个。如果超过,则会分次进行插入。
如果批量插入的时候,中间有一个文档插入失败,那么前面的文档插入成功,而后面的文档则全部插入失败。insertMany
有第三个参数ordered
意味着是否执行有序或者无序的插入,默认为true(执行有序插入),如果为false,则插入的时候会跳过插入失败的数据,继续后面数据的插入。
插入效验
插入数据的时候,Mongo只会对数据进行基本的检查:检查文档的格式, 如果没有 "_id" 字段,就自动增加一个; 检查大小, 所有的文档都必须小于16MB。这样的限制主要是为了防止不良的模式设计, 并且保证性能的一致。
由于MongoDB只进行基本的检查,所以插入非法数据非常容易。因此,应该只允许信任的源连接数据库。主流语言的驱动程序都胡izai数据插入到数据库之前做大量的数据检验(比如文档是否过大,文档是否包含非UTF-8字符串,是否使用了不可识别的类型)。
删除文档
删除的命令是:
db.foo.remove({})
上述命令会删除foo集合中的所有文档,但是不会删除集合本身,也不会删除集合的元信息。remove()
函数可以接受一个查询文档参数作为可选参数。给定这个参数之后,只有符合条件的才会进行删除。
db.foo.remove({"opt-out": true})
删除文档是永久性的,不能撤销,也不能恢复。
删除速度
删除文档通常会快,但是如果要清空整个集合。使用"drop"直接删除这个集合会更快,然后再这个空集合上面重建各项索引。需要注意的是"drop"不能指定任何条件,因为整个集合都被删除,集合的元数据都不见了。
更新文档
更新文档使用的是update
,update
有两个参数,一个是查询文档,需要定位你需要更新的目标文件,一个是修改器文档,用于说明要对找到的文档进行那些修改。
更新操作是不可分割的。若是两个文档更新同时发生,先到达服务器的先执行,接着执行另外一个,所以,两个需要同时进行的更新会迅速接连完成。此过程不会破坏文档。
文档替换
例如要对下面的文档进行一个大的调整
{
"_id" : ObjectId("57745b2294ec519556ea6040"),
"name" : "joe",
"friends" : 32.0,
"enemies" : 2.0
}
我们希望将friends
和enemies
两个字段移到relationships
子文档中,可以这样实现
var joe = db.users.findOne({"name": "joe"});
joe.relationships = {"friends": joe.friends, "enemies": joe.enemies};
joe.username = joe.name;
delete joe.friends;
delete joe.enemies;
delete joe.name;
db.users.update({"name": "joe"}, joe);
现在可以用findOne
来查看更新后的文档数据。
{
"_id" : ObjectId("57745b2294ec519556ea6040"),
"username" : "joe",
"relationships" : {
"friends" : 32.0,
"enemies" : 2.0
}
}
这里面有个问题。就是说,如果不知道有多个同样name=joe
的文档的时候,如果盲目update
,会造成因为多个文档在替换的时候,因为_id
重复了,结果会导致更新失败。这个时候,我们可以使用_id
来作为限定字段,因为_id
在一个集合当中是唯一的。对于上面的例子,这才是正确的更新办法:
db.users.update({"_id": ObjectId("57745b2294ec519556ea6040")}, joe)
使用_id
作为查询条件比使用随机字段速度更快,因集合是通过_id
来建立的索引。
使用修改器
通常文档只有一部分需要更新。我们可以使用原子性的更新修改器,指定对文档中的某些字段进行更新。更新修改器是一种特殊的键,用来指定复杂的更新操作。
$set
修改器
比如用户资料存储在下面的文档里面:
{
"_id" : ObjectId("5778a7e487d2bf26ed1188c4"),
"name" : "joe",
"age" : 30.0,
"sex" : "male",
"location" : "Wisconsin"
}
比如我们想要添加想要的书籍。我们可以这么执行:
db.foo.update({"_id" : ObjectId("5778a7e487d2bf26ed1188c4")}, {"$set": {"favorite book": "War and Peace"}})
然后文档就有了favorite book
键。$set
在key存在的时候就则进行覆盖,如果不存在,则变成新增Key。
{
"_id" : ObjectId("5778a7e487d2bf26ed1188c4"),
"name" : "joe",
"age" : 30.0,
"sex" : "male",
"location" : "Wisconsin",
"favorite book" : "War and Peace"
}
$set
可以改变键的数据类型。比如我们喜欢很多本书。我们可以这么修改。
db.foo.update({"_id" : ObjectId("5778a7e487d2bf26ed1188c4")}, {"$set": {"favorite book": ["Cat's Cradle", "Foundation Trilogy", "Ender's Game"]}})
然后用户不爱看书,可以使用$unset
将这个键完全删除:
db.foo.update({"_id" : ObjectId("5778a7e487d2bf26ed1188c4")}, {"$unset": {"favorite book": 1}})
我们也可以去修改内嵌文档。比如如下文档:
{
"_id" : ObjectId("577906ca0befef90da41a9c6"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe",
"email" : "joe@example.com"
}
}
db.foo.update({"_id" : ObjectId("577906ca0befef90da41a9c6")}, {"$set": {"author.name": "joe schmoe"}})
查看文档:
{
"_id" : ObjectId("577906ca0befef90da41a9c6"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe schmoe",
"email" : "joe@example.com"
}
}
增加和减少
$inc
修改器可以用来增加已有键的值,或者该键不存在,那么就创建一个。
比如我们有这么一个文档。
{
"_id" : ObjectId("57790cfe0befef90da41a9c7"),
"game" : "pinball",
"user" : "joe"
}
比如我们给这个文档增加50
db.foo.update({"_id" : ObjectId("57790cfe0befef90da41a9c7")}, {"$inc": {"score": 50}})
数组追加元素
{
"_id" : ObjectId("5794a4f679b354ae7c0dccad"),
"title" : "a blog post",
"content" : "xxx"
}
我们现在要对这个文档增加评论:
db.foo.update({"_id": ObjectId("5794a4f679b354ae7c0dccad")}, {"$push": {"comments": {'name': "joe", "email": "joe@example.com", "content": "nice post."}}})
我们再一次查看该文档,就变成了这样:
{
"_id" : ObjectId("5794a4f679b354ae7c0dccad"),
"title" : "a blog post",
"content" : "xxx",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post."
}
]
}
如果comment
键不存在,它会创建一个值为数组的comment
键,并向数组中添加一条评论。
如果要同时添加多条评论,我们还可以这么办:
db.foo.update({"_id": ObjectId("5794a4f679b354ae7c0dccad")}, {
"$push": {
"comments": {
"$each": [
{'name': "joe", "email": "joe@example.com", "content": "nice post1."},
{'name': "joe", "email": "joe@example.com", "content": "nice post2."}
]
}
}
})
查看一下文档,就发现已经同时添加了两条评论:
{
"_id" : ObjectId("5794a4f679b354ae7c0dccad"),
"title" : "a blog post",
"content" : "xxx",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post."
},
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post1."
},
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post2."
}
]
}
如果想让我们的comment
最大只能存储4条评论,我们将$slice
和$push
组合在一起使用,这样就可以保证数组不会超过设定好的最大长度:
db.foo.update({"_id": ObjectId("5794a4f679b354ae7c0dccad")}, {
"$push": {
"comments": {
"$each": [
{'name': "joe", "email": "joe@example.com", "content": "nice post1."},
{'name': "joe", "email": "joe@example.com", "content": "nice post2."}
],
"$slice": -4
}
}
})
{
"_id" : ObjectId("5794a4f679b354ae7c0dccad"),
"title" : "a blog post",
"content" : "xxx",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post1."
},
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post2."
},
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post1."
},
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post2."
}
]
}
我们发现最开始插入的那条评论已经不存在了。只保存了最后插入的4条评论。如果我们限制数组只包含最后插入的10个元素。$slice
就必须是负整数。$slice
如果是正的,那么只会保存最开始插入的4条评论。
将数组作为数据集使用
如果我们将数组作为数据集使用,保证数组内的元素不会重复。我们可以使用$addToSet
来实现。
比如我们有下面这个文档:
{
"_id" : ObjectId("5794ad1279b354ae7c0dccae"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com"
]
}
比如我们要给这个文档添加新的邮件地址,我们可以使用$addToSet
来实现避免插入重复地址:
db.foo.update({"_id" : ObjectId("5794ad1279b354ae7c0dccae")}, {
"$addToSet": {
"emails": {
"$each":[
"joe@hotmail.com",
"joe@yahoo.com"
]
}
}
})
我们向文档中插入两个邮箱,我们查看一下文档,我们发现数量只是增加了一个。
{
"_id" : ObjectId("5794ad1279b354ae7c0dccae"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com",
"joe@hotmail.com"
]
}
删除元素
比如上个文档,我们需要删除emails
的一个有邮箱。我们可以使用$pop
来删除。比如{"$pop": {"emails": 1}}
就是从末尾删除一个元素,而{"$pop": {"emails": 2}}
则是从头部进行删除。
而比如我们要根据条件来删除数组中的元素,而不是位置。我们可以使用$pull
,比如我们删除joe@yahoo.com
,我们可以执行下面的命令:
db.foo.update({"_id" : ObjectId("5794ad1279b354ae7c0dccae")}, {
"$pull": {
"emails" : "joe@yahoo.com"
}
})
我们查看一下文档:
{
"_id" : ObjectId("5794ad1279b354ae7c0dccae"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@hotmail.com"
]
}
比如我们想把emails
里面的第一个元素修改一下。我们这样:
db.foo.update({"_id" : ObjectId("5794ad1279b354ae7c0dccae")}, {
"$set": {
"emails.0" : "joes@example.com"
}
})
这样就修改成功了:
{
"_id" : ObjectId("5794ad1279b354ae7c0dccae"),
"username" : "joe",
"emails" : [
"joes@example.com", // 这一行已经正确修改
"joe@gmail.com",
"joe@hotmail.com"
]
}
比如当我们不知道我们要修改的值得位置,我们可以使用$
来自动匹配。
比如我们要修改emails
的joe@gmail.com
为joes@gmail.com
,我们可以这样办:
db.foo.update({"_id" : ObjectId("5794ad1279b354ae7c0dccae"), "emails": "joe@gmail.com"}, {
"$set": {
"emails.$" : "joes@gmail.com"
}
})
然后文档就修改成功了:
{
"_id" : ObjectId("5794ad1279b354ae7c0dccae"),
"username" : "joe",
"emails" : [
"joes@example.com",
"joes@gmail.com",
"joe@hotmail.com"
]
}
修改器速度
有的修改器运行速度很快,比如$inc
,因为它不需要改变文档的大小,只需要将键的值修改一下,所以非常快。
但是$push
会改变文档的大小,所以就会慢一些($set
能在文档大小不改变的时候立即修改它,否则性能也会有所下降)。
将文档插入到MongoDB中的时候,依次插入的文档在磁盘中的位置是相邻的。如果一个文档变大了,之前的位置放不下这个文档了,那么文档就会被移动到集合的另外一个位置。
如果你的模式在进行插入和删除的时会进行大量的移动或者经常打乱数据,可以使用usePowerOf2Sizes
来提高磁盘的复用率。
db.runCommand({"collMod": "集合名称", "usePowerOf2Sizes": true})
执行该命令之后,以后进行的所有空间分配,所得到的块大小都是2的幂。由于这个选项会导致初始空间分配不是那么高效,所以应该只在需要经常打乱数据的集合上面使用。
在一个只进行插入或者原地更新的集合上使用这个选项,会导致写入速度变慢。
upsert
upsert
是一种特殊的更新。要是没有找到符合更新条件的文档,就会以这个条件和更新文档为基础创建一个新的文档。如果找到了匹配的文档,则正常进行更新。 upsert
非常方便,不必预置集合,同一套代码既可以用户创建文档,又可以更新文档。
使用upsert
,既可以避免竞态问题,又可以缩减代码量。具体写法如下:
db.foo.update({"url": "joe"}, {"$inc": {"pageviews": 1}}, true)
update
的第三个参数表示是否使用upsert
,默认是false
。这行代码是原子性的,而且特别高效。
有的时候,我们需要创建文档的时候创建一个字段并为其赋值,但是更新的时候,我们并不需要更新这个字段。我们可以这样办。比如created_at
这个字段,我们仅仅需要在创建文档的时候赋值,不需要进行更新,我们可以执行下列命令:
db.foo.update({"url": "joe"}, {"$setOnInsert": {"created_at": new Date()}}, true)
更新多个文档
默认情况下,更新只能对符合匹配条件的第一个文档执行操作。要是有多个文档符合条件,只会更新第一个文档,其他文档不会发生变化。要更新多个文档,我们可以把update
的第四个参数设置为true。
比如下面的这条命令:
db.foo.update({"birthday": "1978-10-13"}, {"$set": {"gift": "Happy Birthday!"}}, false, ture)
注意:
update
以后的行为可能会发生变化,比如服务器默认只修改一个文档变为默认会更新所有匹配的文档。所以建议显式指定update
的行为或者注意MongoDB
的版本更新变化
返回被更新的文档
以上的命令并不能返回被更新的文档。但是我们可以通过执行findAndModify
命令来获得被更新的文档。
首先介绍一下findAndModify
命令可以使用的字段:
findAndModify
字符串,集合名称query
查询文档,用于检索文档的条件。sort
排序结果的条件update
修改器条件,用户对匹配的文档进行更新(update
和remove
必须指定一个)remove
布尔类型,表示是否删除文档(update
和remove
必须指定一个)new
布尔类型,表示返回更新前的文档还是更新后的文档,默认是更新前的文档。fields
文档中需要返回的字段(可选)upsert
布尔类型,值为true
表示这是一个upsert
。默认是false
注意,update
和remove
必须有一个,也只能有一个。要是没有匹配的文档,这个命令会返回一个错误。
比如之前的命令我们就可以这么写:
db.runCommand({"findAndModify": "foo", "query": {"url": "joe"}, "update": {"$inc": {"pageviews": 1}}, "upsert": true}) //更新文档
db.runCommand({"findAndModify": "foo", "query": {"url": "joe"}, "remove": true}) //删除文档
写入安全机制
写入安全是一种客户端设置,用于控制写入的安全级别。默认情况下,插入、更新和删除都会一直等待数据库响应。然后才会继续执行。如果遇到错误,客户端会抛出一个错误。
两种最基本的写入安全机制是应答式写入和非应答式写入。应答式是默认的方式:数据库会给出响应,告诉你写入操作是否执行成功。非应答式写入不返回任何响应,所以无法知道写入是否成功。
shell与客户端程序对非应答式写入的实际支持不一样:shell在执行非应答式写入后,会检查最后一个操作是否成功,然后才会向用户输出提示信息。因此,如果在集合上执行了一系列无效操作,最后又执行了一个有效操作,shell并不会提示错误。