MongoDB(三):创建、更新和删除文档

本文所有内容以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"不能指定任何条件,因为整个集合都被删除,集合的元数据都不见了。

更新文档

更新文档使用的是updateupdate有两个参数,一个是查询文档,需要定位你需要更新的目标文件,一个是修改器文档,用于说明要对找到的文档进行那些修改。
更新操作是不可分割的。若是两个文档更新同时发生,先到达服务器的先执行,接着执行另外一个,所以,两个需要同时进行的更新会迅速接连完成。此过程不会破坏文档。

文档替换

例如要对下面的文档进行一个大的调整

{
    "_id" : ObjectId("57745b2294ec519556ea6040"),
    "name" : "joe",
    "friends" : 32.0,
    "enemies" : 2.0
}

我们希望将friendsenemies两个字段移到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"
    ]
}

比如当我们不知道我们要修改的值得位置,我们可以使用$来自动匹配。
比如我们要修改emailsjoe@gmail.comjoes@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
    修改器条件,用户对匹配的文档进行更新(updateremove必须指定一个)

  • remove
    布尔类型,表示是否删除文档(updateremove必须指定一个)

  • new
    布尔类型,表示返回更新前的文档还是更新后的文档,默认是更新前的文档。

  • fields
    文档中需要返回的字段(可选)

  • upsert
    布尔类型,值为true表示这是一个upsert。默认是false

注意,updateremove必须有一个,也只能有一个。要是没有匹配的文档,这个命令会返回一个错误。
比如之前的命令我们就可以这么写:

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并不会提示错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值