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的写操作,并且介绍了多数非常常用的方法,下一部分将会详细的探讨查询相关的操作。