MongoDB的數(shù)組和對象類型
在MongoDB中可以添加數(shù)組和對象類型,這其實就對應(yīng)了關(guān)系數(shù)據(jù)庫中OneToMany和ManyToOne的類型,先來看如下一條數(shù)據(jù):
db.topic.insertOne(
{
title:'MongoDB學(xué)習(xí)指南',
tags:['java','程序設(shè)計','數(shù)據(jù)庫','NOSql'],
content:'MongoDB學(xué)習(xí)之道,MongoDB是一個流行的NoSql數(shù)據(jù)庫',
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幾個信息,我們可以發(fā)現(xiàn),對應(yīng)MongoDB而言,除了添加基本數(shù)據(jù)類型之外還可以添加數(shù)組類型,tags數(shù)據(jù)就是一個數(shù)組類型,里面有四個值,而且author是一個對象類型。comments是一個數(shù)組,數(shù)組中的值又是一個對象。這就是MongoDB(文檔型數(shù)據(jù)庫)的特點,如果使用的是關(guān)系型數(shù)據(jù)庫,需要在數(shù)據(jù)庫中加入多張表來進(jìn)行存儲,之后通過join來查詢,而使用文檔型的數(shù)據(jù)庫對于這種結(jié)構(gòu)優(yōu)勢明顯,數(shù)據(jù)庫的設(shè)計問題將會在后面的章節(jié)詳細(xì)探討。
此時可以通過.來導(dǎo)航查詢想要的信息
>db.topic.find({"author.name":"konghao"}) ##查詢作者的名字叫konghao的用戶
{
"_id" : ObjectId("5a12e8cb15ec7ee2ec16bbbb"),
"title" : "MongoDB學(xué)習(xí)指南",
"tags" : [
"java",
"程序設(shè)計",
"數(shù)據(jù)庫",
"NOSql"
],
"content" : "MongoDB學(xué)習(xí)之道,MongoDB是一個流行的NoSql數(shù)據(jù)庫",
"comments" : [
{
"user" : "jake",
"content" : "Good Text"
},
{
"user" : "Leon",
"content" : "Ok!!"
}
],
"author" : {
"name" : "konghao",
"mail" : "ynkonghao@gmail.com"
}
}
我們還可以利用javascript shell來查詢對象中的數(shù)組信息
> var d = db.topic.findOne(); ##將topic中的第一個條數(shù)據(jù)存儲到變量d中
> d.comments.length ##獲取comments的長度
2
> d.comments ##顯示所有的comments
[
{
"user" : "jake",
"content" : "Good Text"
},
{
"user" : "Leon",
"content" : "Ok!!"
}
]
MongoDB的批量插入操作
首先是關(guān)于主鍵的操作,如果添加數(shù)據(jù)的時候不添加任何主鍵,會自動生成一個主鍵,這個主鍵并不像關(guān)系數(shù)據(jù)庫中的自動遞增(這是為了分布式考慮的),而是使用"時間戳+機(jī)器編號+進(jìn)程編號+序列號"來生成的,這就可以保證每個id都是唯一的。id為5a0ed29a0149542ec19d4dd8,可以這樣分解:5a0ed29a 014954 2ec1 9d4dd8 首先前四個字節(jié)(5a0ed29a)表示時間戳,014954表示機(jī)器號,2ec1表示進(jìn)程編號,最后的9d4dd8表示序列號。
我們也可以手動指定id,使用_id來指定
> db.user.insertOne({_id:"user001",name:"foobar"}) ##插入一條數(shù)據(jù),id是自己指定
{ "acknowledged" : true, "insertedId" : "user001" }
> db.user.find({name:"foobar"}) ##找到id為自己指定的數(shù)據(jù)
{ "_id" : "user001", "name" : "foobar" }
通過db.collection.insertMany()可以批量插入數(shù)據(jù),MongoDB的批量插入效率比較高,它執(zhí)行的操作非常少,它首先會查詢批量插入的文檔中是否包含有 _ id, 如果包含有,就驗證_id是否已經(jīng)存在,當(dāng)發(fā)現(xiàn)所有的id都不存在,就直接存儲,此時不會做其他的檢查,這樣就保證了插入的效率,當(dāng)然這種也會存在一些問題,就是可能存在不合理的空數(shù)據(jù)存在。
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的刪除操作
刪除操作上一講已經(jīng)介紹了,在3.2版本之前可以通過db.colletion.remove()來刪除,在3.2之后提供db.collection.deleteOne()來刪除一個數(shù)據(jù)和db.collection.deleteMany()來完成批量刪除。在新版本中要進(jìn)行刪除操作,需要加入過濾條件,如果要刪除所有的信息使用db.collection.deleteMany({})即可刪除所有數(shù)據(jù)。
> db.user.deleteMany({}) ##刪除所有數(shù)據(jù)
{ "acknowledged" : true, "deletedCount" : 6 }
> db.user.find()
>
接下來我們看一下批量刪除的速度問題,首先批量添加一些數(shù)據(jù)
for(int i=0;i<1000000;i++) {
Document d = new Document("name", "foo")
.append("no", i);
collection.insertOne(d);
}
以上代碼通過java代碼插入了一百萬條數(shù)據(jù),之后我們來進(jìn)行一下刪除操作,使用deleteMany({})來刪除
> use demo
switched to db demo
> db.user.count()
1000000
> db.user.deleteMany({})
{ "acknowledged" : true, "deletedCount" : 1000000 }
>
100W條數(shù)據(jù)花去了18.35秒的時間,這個操作并不會刪除索引,所以時間較長,可以使用db.collection.drop()來完全刪除collection,效率就會高很多,這樣就需要重新創(chuàng)建索引,而且不能使用其他條件。
> db.user.count()
1000000
> db.user.drop()
true
這次刪除僅僅花了0.1秒的時間,所以如果希望全部刪除一個collection,使用drop顯然效率要高得多。
MongoDB的更新操作
MongoDB的更新方式比較多,首先介紹基于shell的方式,這種方式是把數(shù)據(jù)先讀出來,之后以對象的方式完成修改。這種方式一般用在修改較大的情況下。
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} ##設(shè)置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"}) ##查詢看看結(jié)果
{
"_id" : ObjectId("5a155e7156d8db3756cafce3"),
"nickname" : "bar",
"relations" : {
"friends" : 12,
"enemies" : 2
},
"username" : "foo"
}
這就是替換的方式,它是基于一種編程的思想來進(jìn)行,使用這種方式要進(jìn)行單個對象的復(fù)雜修改比較適用。
接著介紹基于修改器的方法,這種方式使用比較廣泛,它的主要思路就是通過$符號來進(jìn)行修改這些操作,首先使用inc可以對數(shù)據(jù)進(jìn)行增加和減少,這個操作只能針對數(shù)字類型,小數(shù)或者整數(shù)
> db.topic.insertOne({title:"first",visites:107})##添加一條數(shù)據(jù)
> db.topic.update({title:"first"},{$inc:{visites:1}})##修改參觀次數(shù)增加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}})##將數(shù)字修改了減少3
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.topic.findOne({title:"first"})
{
"_id" : ObjectId("5a15608556d8db3756cafce5"),
"title" : "first",
"visites" : 105
}
>
使用set可以完成特定需求的修改,這是非常常用的一種方式
> db.author.findOne()##原數(shù)據(jù)
{
"_id" : ObjectId("5a162f8956d8db3756cafce7"),
"name" : "foo",
"age" : 20,
"gender" : "male",
"intro" : "student"
}
> db.author.update({name:"foo"},{$set:{intro:"teacher"}})##使用$set進(jìn)行修改
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
set不僅僅可以修改變量,還可以修改數(shù)據(jù)類型,下面將intro修改為數(shù)組類型
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還可以修改關(guān)聯(lián)類型的值
> db.topic.findOne() ##修改前的數(shù)據(jù)
{
"_id" : ObjectId("5a12e8cb15ec7ee2ec16bbbb"),
"title" : "MongoDB學(xué)習(xí)指南",
"content" : "MongoDB學(xué)習(xí)之道,MongoDB是一個流行的NoSql數(shù)據(jù)庫",
"author" : {
"name" : "konghao",
"mail" : "ynkonghao@gmail.com"
}
}
> db.topic.update(
{"author.name":"konghao"},
{$set:{"author.mail":"konghao@gmail.com"}}
)##修改關(guān)聯(lián)類型author
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.topic.findOne()##修改后的數(shù)據(jù)
{
"_id" : ObjectId("5a12e8cb15ec7ee2ec16bbbb"),
"title" : "MongoDB學(xué)習(xí)指南",
"content" : "MongoDB學(xué)習(xí)之道,MongoDB是一個流行的NoSql數(shù)據(jù)庫",
"author" : {
"name" : "konghao",
"mail" : "konghao@gmail.com"
}
}
更新操作除了針對基本數(shù)據(jù)外還可以更新數(shù)組信息,使用push可以完成數(shù)組的插入,會在最后一條插入,如果沒有這個key,會自動創(chuàng)建一條數(shù)據(jù)插入
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的回復(fù),此時沒有comments。所以創(chuàng)建了一個數(shù)組類型的key出來
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.posts.findOne()##查詢結(jié)果
{
"_id" : ObjectId("5a1656e656d8db3756cafce8"),
"title" : "a blog",
"content" : "...",
"author" : "foo",
"comments" : [
{
"name" : "leon",
"email" : "leon.email.com",
"content" : "leon replay"
}
]
}
使用addToSet可以向一個數(shù)組中添加元素,但這個有一個限定條件就是,如果已經(jīng)存在就不添加,這是非常好用的方法。
> db.users.findOne() ##原始數(shù)據(jù)
{
"_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的數(shù)據(jù)
{
"_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"}})##繼續(xù)添加一條已經(jīng)存在的信息
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可以添加一個數(shù)組,此時如果配合addToSet會非常方便,此時只會添加不存在的數(shù)據(jù),以上一例的數(shù)據(jù)為藍(lán)本
db.users.update(
{name:"foo"},
{$addToSet:{email:{$each:["foo@qq.com","foo@sohu.com"]}}}
)##在email中的添加qq和sohu的郵箱,由于qq已經(jīng)存在,所以不會添加,只會添加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可以刪除數(shù)組中的元素,可以指定從哪一個位置刪除。'{$pop:{xxx:1}}'表示刪除xx這個數(shù)組的最后一個元素,使用xxx:-1表示刪除第一個元素。
> db.users.findOne()##原始數(shù)據(jù)
{
"_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方法而言,第一個參數(shù)是條件,第二個參數(shù)是具體的修改信息,它還有第三個參數(shù),是一個布爾類型的值,如果為true,表示修改操作,如果沒有找到條件,會自動創(chuàng)建需要修改的數(shù)據(jù),默認(rèn)是false。先看看如下實例
> db.statistics.update({url:"/blog"},{$inc:{visites:1}}) ##增加/blog地址的訪問次數(shù)
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 }) ##由于沒有數(shù)據(jù),所以不會更新
> db.statistics.findOne()##沒有更新
null
> db.statistics.update({url:"/blog"},{$inc:{visites:1}},true)##設(shè)置第三個參數(shù)為true,會自動添加
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("5a16ea57d76cb43bc7d5de15")
})
> db.statistics.findOne()##添加了一條數(shù)據(jù)
{
"_id" : ObjectId("5a16ea57d76cb43bc7d5de15"),
"url" : "/blog",
"visites" : 1
}
已上操作非常方便,因為這個操作是原子性的,如果我們使用shell來更新,需要先取出對象,之后判斷,如果存在執(zhí)行更新,不存在執(zhí)行添加,這個操作的步驟較多而且不是原子性。不僅效率不高,而且如果更新到某一個部分拋出異常就會出現(xiàn)數(shù)據(jù)不統(tǒng)一操作。
接下來介紹save這個shell腳本,也非常的實用,它會檢查數(shù)據(jù)是否存在,如果存在就更新,如果不存在就插入,這也是比較實用的操作。
> db.users.findOne()##原始數(shù)據(jù)
{
"_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存在會執(zhí)行修改
44
> foo.num = 33##num不存在會自動添加這個屬性
33
> db.users.save(foo)##執(zhí)行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條數(shù)據(jù),最后的時間是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條數(shù)據(jù),我們發(fā)現(xiàn)時間16秒左右,稍微慢一點點,這說明只要在文檔結(jié)構(gòu)不做調(diào)整的情況下,set的速度也是非常理想的。最后來測試push操作,push僅僅操作1W條數(shù)數(shù)據(jù)
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秒左右,我們發(fā)現(xiàn),push操作的效率比較的低,所以需要注意push的瓶頸問題,必要時可以將數(shù)組拿出來放到一個單獨的集合中。
這一部分詳細(xì)的探討了MongoDB的寫操作,并且介紹了多數(shù)非常常用的方法,下一部分將會詳細(xì)的探討查詢相關(guān)的操作。