MongoDB第三讲 深入MongoDB的插入、删除和更新操作

MongoDB的数组和对象类型

在MongoDB中可以添加数组和对象类型,这其实就对应了关系数据库中OneToMany和ManyToOne的类型,先来看如下一条数据:

db.topic.insertOne(
{
        title:'MongoDB学习指南',
        tags:['java','程序设计','数据库','NOSql'],
        content:'MongoDB学习之道,MongoDB是一个流行的NoSql数据库',
        comments:[
            {
               user:'jake',
               content:'Good Text'
            },
            {
               user:'Leon',
               content:'Ok!!'
            }
        ],
        author:{
            name:'konghao',
            mail:'ynkonghao@gmail.com'
        }
    }
);

以上这条语句插入了一个topic的collection,在这个collection插入了一个文档,这个文档中有title,tags,conent,comments和author几个信息,我们可以发现,对应MongoDB而言,除了添加基本数据类型之外还可以添加数组类型,tags数据就是一个数组类型,里面有四个值,而且author是一个对象类型。comments是一个数组,数组中的值又是一个对象。这就是MongoDB(文档型数据库)的特点,如果使用的是关系型数据库,需要在数据库中加入多张表来进行存储,之后通过join来查询,而使用文档型的数据库对于这种结构优势明显,数据库的设计问题将会在后面的章节详细探讨。

此时可以通过.来导航查询想要的信息

>db.topic.find({"author.name":"konghao"}) ##查询作者的名字叫konghao的用户
{
        "_id" : ObjectId("5a12e8cb15ec7ee2ec16bbbb"),
        "title" : "MongoDB学习指南",
        "tags" : [
                "java",
                "程序设计",
                "数据库",
                "NOSql"
        ],
        "content" : "MongoDB学习之道,MongoDB是一个流行的NoSql数据库",
        "comments" : [
                {
                        "user" : "jake",
                        "content" : "Good Text"
                },
                {
                        "user" : "Leon",
                        "content" : "Ok!!"
                }
        ],
        "author" : {
                "name" : "konghao",
                "mail" : "ynkonghao@gmail.com"
        }
}

我们还可以利用javascript shell来查询对象中的数组信息

> var d = db.topic.findOne(); ##将topic中的第一个条数据存储到变量d中
> d.comments.length ##获取comments的长度
2
> d.comments ##显示所有的comments
[
        {
                "user" : "jake",
                "content" : "Good Text"
        },
        {
                "user" : "Leon",
                "content" : "Ok!!"
        }
]

MongoDB的批量插入操作

首先是关于主键的操作,如果添加数据的时候不添加任何主键,会自动生成一个主键,这个主键并不像关系数据库中的自动递增(这是为了分布式考虑的),而是使用"时间戳+机器编号+进程编号+序列号"来生成的,这就可以保证每个id都是唯一的。id为5a0ed29a0149542ec19d4dd8,可以这样分解:5a0ed29a 014954 2ec1 9d4dd8 首先前四个字节(5a0ed29a)表示时间戳,014954表示机器号,2ec1表示进程编号,最后的9d4dd8表示序列号。

我们也可以手动指定id,使用_id来指定

> db.user.insertOne({_id:"user001",name:"foobar"}) ##插入一条数据,id是自己指定
{ "acknowledged" : true, "insertedId" : "user001" }
> db.user.find({name:"foobar"}) ##找到id为自己指定的数据
{ "_id" : "user001", "name" : "foobar" }

通过db.collection.insertMany()可以批量插入数据,MongoDB的批量插入效率比较高,它执行的操作非常少,它首先会查询批量插入的文档中是否包含有 _ id, 如果包含有,就验证_id是否已经存在,当发现所有的id都不存在,就直接存储,此时不会做其他的检查,这样就保证了插入的效率,当然这种也会存在一些问题,就是可能存在不合理的空数据存在。

db.user.insertMany([
...     {username:"foo",nickname:"FOO",password:"123"},
...     {username:"bar",nickname:"BAR",password:"111"},
...     {username:"hello",nickname:"HELLO",password:"123"}
... ]
... )
{
        "acknowledged" : true,
        "insertedIds" : [
                ObjectId("5a12fcf46533217cfe3d4090"),
                ObjectId("5a12fcf46533217cfe3d4091"),
                ObjectId("5a12fcf46533217cfe3d4092")
        ]
}

MongoDB的删除操作

删除操作上一讲已经介绍了,在3.2版本之前可以通过db.colletion.remove()来删除,在3.2之后提供db.collection.deleteOne()来删除一个数据和db.collection.deleteMany()来完成批量删除。在新版本中要进行删除操作,需要加入过滤条件,如果要删除所有的信息使用db.collection.deleteMany({})即可删除所有数据。

> db.user.deleteMany({}) ##删除所有数据
{ "acknowledged" : true, "deletedCount" : 6 }
> db.user.find()
>

接下来我们看一下批量删除的速度问题,首先批量添加一些数据

for(int i=0;i<1000000;i++) {
  Document d = new Document("name", "foo")
  .append("no", i);
  collection.insertOne(d);
}

以上代码通过java代码插入了一百万条数据,之后我们来进行一下删除操作,使用deleteMany({})来删除

> use demo
switched to db demo
> db.user.count()
1000000
> db.user.deleteMany({})
{ "acknowledged" : true, "deletedCount" : 1000000 }
>

100W条数据花去了18.35秒的时间,这个操作并不会删除索引,所以时间较长,可以使用db.collection.drop()来完全删除collection,效率就会高很多,这样就需要重新创建索引,而且不能使用其他条件。

> db.user.count()
1000000
> db.user.drop()
true

这次删除仅仅花了0.1秒的时间,所以如果希望全部删除一个collection,使用drop显然效率要高得多。

MongoDB的更新操作

MongoDB的更新方式比较多,首先介绍基于shell的方式,这种方式是把数据先读出来,之后以对象的方式完成修改。这种方式一般用在修改较大的情况下。

db.user.insertOne({
    name:"foo",
    nickname:"bar",
    friends:12,
    enemies:2
})

我们希望修改为

db.user.insertOne({
    username:"foo",
    nickname:"bar",
    relations:{
        friends:12,
        enemies:2
    }
})

这种使用shell的替换方式比较方便

var u = db.user.findOne({name:"foo"}) ##查询对象存储到u中
u.relations = {friends:u.friends,enemies:u.enemies} ##设置relations的值
u.username = u.name ##修改name为username
delete u.friends ##删除friends
delete u.enemies ##删除enemies
delete u.name ##删除name
db.user.update({name:"foo"},u) ##替换对象
db.user.findOne({username:"foo"}) ##查询看看结果
{
        "_id" : ObjectId("5a155e7156d8db3756cafce3"),
        "nickname" : "bar",
        "relations" : {
                "friends" : 12,
                "enemies" : 2
        },
        "username" : "foo"
}

这就是替换的方式,它是基于一种编程的思想来进行,使用这种方式要进行单个对象的复杂修改比较适用。

接着介绍基于修改器的方法,这种方式使用比较广泛,它的主要思路就是通过$符号来进行修改这些操作,首先使用inc可以对数据进行增加和减少,这个操作只能针对数字类型,小数或者整数

> db.topic.insertOne({title:"first",visites:107})##添加一条数据
> db.topic.update({title:"first"},{$inc:{visites:1}})##修改参观次数增加1
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.topic.findOne({title:"first"})##查询
{
        "_id" : ObjectId("5a15608556d8db3756cafce5"),
        "title" : "first",
        "visites" : 108
}
> db.topic.update({title:"first"},{$inc:{visites:-3}})##将数字修改了减少3
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.topic.findOne({title:"first"})
{
        "_id" : ObjectId("5a15608556d8db3756cafce5"),
        "title" : "first",
        "visites" : 105
}
>

使用set可以完成特定需求的修改,这是非常常用的一种方式

> db.author.findOne()##原数据
{
        "_id" : ObjectId("5a162f8956d8db3756cafce7"),
        "name" : "foo",
        "age" : 20,
        "gender" : "male",
        "intro" : "student"
}
> db.author.update({name:"foo"},{$set:{intro:"teacher"}})##使用$set进行修改
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

set不仅仅可以修改变量,还可以修改数据类型,下面将intro修改为数组类型

 db.author.update({name:"foo"},{$set:{intro:["teacher","programmer"]}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.author.findOne()
{
        "_id" : ObjectId("5a162f8956d8db3756cafce7"),
        "name" : "foo",
        "age" : 20,
        "gender" : "male",
        "intro" : [
                "teacher",
                "programmer"
        ]
}

使用unset可以删除一个键,由于操作的时候都要通过写键值对,所以,可以在要删除的字段后随便写个值即可,一下代码写了一个1

> db.author.update({name:"foo"},{$unset:{intro:1}})##删除intro属性
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.author.findOne()
{
        "_id" : ObjectId("5a162f8956d8db3756cafce7"),
        "name" : "foo",
        "age" : 20,
        "gender" : "male"
}

使用set还可以修改关联类型的值

> db.topic.findOne() ##修改前的数据
{
        "_id" : ObjectId("5a12e8cb15ec7ee2ec16bbbb"),
        "title" : "MongoDB学习指南",
        "content" : "MongoDB学习之道,MongoDB是一个流行的NoSql数据库",
        "author" : {
                "name" : "konghao",
                "mail" : "ynkonghao@gmail.com"
        }
}
> db.topic.update(
    {"author.name":"konghao"},
    {$set:{"author.mail":"konghao@gmail.com"}}
)##修改关联类型author
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.topic.findOne()##修改后的数据
{
        "_id" : ObjectId("5a12e8cb15ec7ee2ec16bbbb"),
        "title" : "MongoDB学习指南",
        "content" : "MongoDB学习之道,MongoDB是一个流行的NoSql数据库",
        "author" : {
                "name" : "konghao",
                "mail" : "konghao@gmail.com"
        }
}

更新操作除了针对基本数据外还可以更新数组信息,使用push可以完成数组的插入,会在最后一条插入,如果没有这个key,会自动创建一条数据插入

db.posts.findOne()
{
        "_id" : ObjectId("5a1656e656d8db3756cafce8"),
        "title" : "a blog",
        "content" : "...",
        "author" : "foo"
}
> db.posts.update({title:"a blog"},
    {$push:{comments:{name:"leon",email:"leon.email.com",content:"leon replay"}}})
    ##插入了一条comments的回复,此时没有comments。所以创建了一个数组类型的key出来
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.posts.findOne()##查询结果
{
        "_id" : ObjectId("5a1656e656d8db3756cafce8"),
        "title" : "a blog",
        "content" : "...",
        "author" : "foo",
        "comments" : [
                {
                        "name" : "leon",
                        "email" : "leon.email.com",
                        "content" : "leon replay"
                }
        ]
}

使用addToSet可以向一个数组中添加元素,但这个有一个限定条件就是,如果已经存在就不添加,这是非常好用的方法。

> db.users.findOne() ##原始数据
{
        "_id" : ObjectId("5a1659a756d8db3756cafce9"),
        "name" : "foo",
        "age" : 12,
        "email" : [
                "foo@example.com",
                "foo@163.com"
        ]
}
> db.users.update({name:"foo"},{$addToSet:{email:"foo@qq.com"}})##添加了一个email信息
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.update({name:"foo"},{$addToSet:{email:"foo@gmail.com"}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.findOne()##添加完成之后多了两条email的数据
{
        "_id" : ObjectId("5a1659a756d8db3756cafce9"),
        "name" : "foo",
        "age" : 12,
        "email" : [
                "foo@example.com",
                "foo@163.com",
                "foo@qq.com",
                "foo@gmail.com"
        ]
}
> db.users.update({name:"foo"},{$addToSet:{email:"foo@gmail.com"}})##继续添加一条已经存在的信息
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 0 })
> db.users.findOne()##并没有添加成功
{
        "_id" : ObjectId("5a1659a756d8db3756cafce9"),
        "name" : "foo",
        "age" : 12,
        "email" : [
                "foo@example.com",
                "foo@163.com",
                "foo@qq.com",
                "foo@gmail.com"
        ]
}
>

使用each可以添加一个数组,此时如果配合addToSet会非常方便,此时只会添加不存在的数据,以上一例的数据为蓝本

db.users.update(
    {name:"foo"},
    {$addToSet:{email:{$each:["foo@qq.com","foo@sohu.com"]}}}
)##在email中的添加qq和sohu的邮箱,由于qq已经存在,所以不会添加,只会添加sohu的邮箱
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.findOne()
{
        "_id" : ObjectId("5a165d5f56d8db3756cafcea"),
        "name" : "foo",
        "age" : 12,
        "email" : [
                "foo@example.com",
                "foo@163.com",
                "foo@gmail.com",
                "foo@qq.com",
                "foo@sohu.com"
        ]
}

通过pop可以删除数组中的元素,可以指定从哪一个位置删除。'{$pop:{xxx:1}}'表示删除xx这个数组的最后一个元素,使用xxx:-1表示删除第一个元素。

> db.users.findOne()##原始数据
{
        "_id" : ObjectId("5a165d5f56d8db3756cafcea"),
        "email" : [
                "foo@example.com",
                "foo@163.com",
                "foo@gmail.com",
                "foo@qq.com",
                "foo@sohu.com"
        ]
}
> db.users.update({name:"foo"},{$pop:{email:1}})##删除email最后一个位置元素,sohu会被删除
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.findOne()
{
        "_id" : ObjectId("5a165d5f56d8db3756cafcea"),
        "email" : [
                "foo@example.com",
                "foo@163.com",
                "foo@gmail.com",
                "foo@qq.com"
        ]
}
>db.users.update({name:"foo"},{$pop:{email:-1}})#使用-1删除第一个元素
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.findOne()
{
        "_id" : ObjectId("5a165d5f56d8db3756cafcea"),
        "name" : "foo",
        "age" : 12,
        "email" : [
                "foo@163.com",
                "foo@gmail.com",
                "foo@qq.com"
        ]
}

对于update方法而言,第一个参数是条件,第二个参数是具体的修改信息,它还有第三个参数,是一个布尔类型的值,如果为true,表示修改操作,如果没有找到条件,会自动创建需要修改的数据,默认是false。先看看如下实例

> db.statistics.update({url:"/blog"},{$inc:{visites:1}}) ##增加/blog地址的访问次数
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 }) ##由于没有数据,所以不会更新
> db.statistics.findOne()##没有更新
null
> db.statistics.update({url:"/blog"},{$inc:{visites:1}},true)##设置第三个参数为true,会自动添加
WriteResult({
        "nMatched" : 0,
        "nUpserted" : 1,
        "nModified" : 0,
        "_id" : ObjectId("5a16ea57d76cb43bc7d5de15")
})
> db.statistics.findOne()##添加了一条数据
{
        "_id" : ObjectId("5a16ea57d76cb43bc7d5de15"),
        "url" : "/blog",
        "visites" : 1
}

已上操作非常方便,因为这个操作是原子性的,如果我们使用shell来更新,需要先取出对象,之后判断,如果存在执行更新,不存在执行添加,这个操作的步骤较多而且不是原子性。不仅效率不高,而且如果更新到某一个部分抛出异常就会出现数据不统一操作。

接下来介绍save这个shell脚本,也非常的实用,它会检查数据是否存在,如果存在就更新,如果不存在就插入,这也是比较实用的操作。

> db.users.findOne()##原始数据
{
        "_id" : ObjectId("5a165d5f56d8db3756cafcea"),
        "name" : "foo",
        "age" : 33,
        "email" : [
                "foo@163.com",
                "foo@gmail.com",
                "foo@qq.com"
        ]
}
> var foo=db.users.findOne({name:"foo"})
> foo.age = 44##修改age为44,age存在会执行修改
44
> foo.num = 33##num不存在会自动添加这个属性
33
> db.users.save(foo)##执行save方法
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.find({name:"foo"}).pretty()
{
        "_id" : ObjectId("5a165d5f56d8db3756cafcea"),
        "name" : "foo",
        "age" : 44,##修改的变量
        "email" : [
                "foo@163.com",
                "foo@gmail.com",
                "foo@qq.com"
        ],
        "num" : 33 ##新添加的
}
>

最后来实际测试一下更新的效率问题,这里将会测试inc,set和push三个方法

long start = new Date().getTime();
for(int i=0;i<100000;i++) {
  collection.updateOne(gte("num",1),inc("num",1));//使用java完成更新
}
long end = new Date().getTime();
System.out.println((end-start)/1000);

插入了10W条数据,最后的时间是13秒左右,差不多每秒7000多条,对于一个台个人电脑而言效率还算不错,接着测试set操作。

long start = new Date().getTime();
for(int i=0;i<100000;i++) {
  collection.updateOne(eq("name","leon"),set("num",i));//使用java完成修改
}
long end = new Date().getTime();
System.out.println((end-start)/1000);

同样操作10W条数据,我们发现时间16秒左右,稍微慢一点点,这说明只要在文档结构不做调整的情况下,set的速度也是非常理想的。最后来测试push操作,push仅仅操作1W条数数据

long start = new Date().getTime();
for(int i=0;i<10000;i++) {
  collection.updateOne(eq("name","Ada"),push("nums",i));
}
long end = new Date().getTime();
System.out.println((end-start)/1000);

时间约为25秒左右,我们发现,push操作的效率比较的低,所以需要注意push的瓶颈问题,必要时可以将数组拿出来放到一个单独的集合中。

这一部分详细的探讨了MongoDB的写操作,并且介绍了多数非常常用的方法,下一部分将会详细的探讨查询相关的操作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容