MongoDB權威指南(2)- 新增、修改、刪除操作
1.插入和保存document
如前所述,向collection插入document使用insert方法
如果document里邊沒有"_id"鍵,"_id"會被自動創建
批量插入
批量插入是一種更高效的方法,傳遞給database一個document的數組,可以一次插入多個document。單個插入的時候,向 database傳送一個document,前邊會附加一個頭部,告訴database在某個collection執行一次插入操作。批量插入只產生一個 TCP請求,意味著不用處理很多請求,同時也省掉了處理頭部的時間。批量插入只能插入到一個collection里邊去。
批量插入只能用于應用程序接口,shell不支持(至少到目前還不支持)。
另外,如果想導入數據(比如說從mysql),不要使用批量插入,使用命令行工具如mongoimport。
2.刪除document
這個命令會刪除users里邊的所有document。
remove函數可以有一個查詢用document做參數,以刪除符合條件的document。
這個命令刪除所有"opt-out"為true的document。
刪除文檔通常是一個非常快的操作,如果想清除整個collection,還有一種更快的方法,使用drop函數然后重建索引。
3.更新document
udpate方法可以攜帶兩個參數:
- 查詢用document, 用于定位哪些document將會被更新
- 修飾符document,用于描述如何修改找到的document
更新是原子性操作,先到達服務器的將會被先執行,后到達的會被后執行,所以,后邊的會覆蓋前邊的修改。
document替換
使用一個新的document來替換匹配的,上一篇文章里用的其實就是document替換,如
document替換時一個常見的錯誤是當有多個匹配的document時候可能會導致duplicate key錯誤。舉個例子,
假設我們有好幾個名字都叫joe的document,
{ " _id " : ObjectId( " 4b2b9f67a1f631733d917a7b " ), " name " : " joe " , " age " : 65 },
{ " _id " : ObjectId( " 4b2b9f67a1f631733d917a7c " ), " name " : " joe " , " age " : 20 },
{ " _id " : ObjectId( " 4b2b9f67a1f631733d917a7d " ), " name " : " joe " , " age " : 49 },
現在,2號joe(20歲那個)生日到了,我們要給他的年齡加1,
{
" _id " : ObjectId( " 4b2b9f67a1f631733d917a7c " ),
" name " : " joe " ,
" age " : 20
}
> joe.age ++ ;
> db.people.update({ " name " : " joe " }, joe);
E11001 duplicate key on update
Oh,出錯了,怎么回事?數據庫查找name為joe的document,找到的第一個是65歲那個,然后試圖替換這個document,然而
數據庫里邊已經有一個"_id"為"4b2b9f67a1f631733d917a7c" 的記錄了,”_id"是不可重復的,所以就有個這個錯誤。
所以執行document替換的時候要小心,確認你要替換的是唯一一個符合條件的。
使用修飾符
通常情況下我們只想更新document的一部分,我們可以使用更新修飾符來做到這一點。
假設我們有一個記錄網站訪問信息的一個collection,里邊的document像這個樣子
" _id " : ObjectId( " 4b253b067525f35f94b60a31 " ),
" url " : " www.example.com " ,
" pageviews " : 52
}
pageviews是站點的訪問次數,那么我想給它增加1的時候就可以這樣子做
... { " $inc " : { " pageviews " : 1 }})
"$inc"就是個更新修飾符,使用更新修飾符的時候,不能更新"_id"鍵的值。
下邊我們看看常用的更新修飾符
- $set
$set修飾符設定指定的key的值,如果key不存在就創建一個
假設我們有下邊一個用戶檔案
> db.users.findOne()
{
" _id " : ObjectId( " 4b253b067525f35f94b60a31 " ),
" name " : " joe " ,
" age " : 30 ,
" sex " : " male " ,
" location " : " Wisconsin "
}
> db.users.update({ " _id " : ObjectId( " 4b253b067525f35f94b60a31 " )},
... { " $set " : { " favorite book " : " war and peace " }})
> db.users.findOne()
{
" _id " : ObjectId( " 4b253b067525f35f94b60a31 " ),
" name " : " joe " ,
" age " : 30 ,
" sex " : " male " ,
" location " : " Wisconsin " ,
" favorite book " : " war and peace "
}
> db.users.update({ " name " : " joe " },
... { " $set " : { " favorite book " : " green eggs and ham " }})
> db.users.update({ " name " : " joe " },
... { " $set " : { " favorite book " :
... [ " cat's cradle " , " foundation trilogy " , " ender's game " ]}})
> db.users.update({ " name " : " joe " },
... { " $unset " : { " favorite book " : 1 }})
- $inc
$inc修飾符只能用于數字,增加一個指定的數量
- 數組修飾符$push
$push向數組尾部追加一個元素,如果數組不存在則創建一個數組
例如,我想給一篇博客文章添加評論,而評論這個key還不存在
> db.blog.posts.findOne()
{
" _id " : ObjectId( " 4b2d75476cc613d5ee930164 " ),
" title " : " A blog post " ,
" content " : " ... "
}
> db.blog.posts.update({ " title " : " A blog post " }, {$push : { " comments " :
... { " name " : " joe " , " email " : " joe@example.com " , " content " : " nice post. " }}})
> db.blog.posts.findOne()
{
" _id " : ObjectId( " 4b2d75476cc613d5ee930164 " ),
" title " : " A blog post " ,
" content " : " ... " ,
" comments " : [
{
" name " : " joe " ,
" email " : " joe@example.com " ,
" content " : " nice post. "
}
]
}
> db.blog.posts.update({ " title " : " A blog post " }, {$push : { " comments " :
... { " name " : " bob " , " email " : " bob@example.com " , " content " : " good post. " }}})
> db.blog.posts.findOne()
{
" _id " : ObjectId( " 4b2d75476cc613d5ee930164 " ),
" title " : " A blog post " ,
" content " : " ... " ,
" comments " : [
{
" name " : " joe " ,
" email " : " joe@example.com " ,
" content " : " nice post. "
},
{
" name " : " bob " ,
" email " : " bob@example.com " ,
" content " : " good post. "
}
]
}
> db.papers.update({ " authors cited " : { " $ne " : " Richie " }},
... {$push : { " authors cited " : " Richie " }})
> db.users.findOne({ " _id " : ObjectId( " 4b2d75476cc613d5ee930164 " )})
{
" _id " : ObjectId( " 4b2d75476cc613d5ee930164 " ),
" username " : " joe " ,
" emails " : [
" joe@example.com " ,
" joe@gmail.com " ,
" joe@yahoo.com "
]
}
> db.users.update({ " _id " : ObjectId( " 4b2d75476cc613d5ee930164 " )},
... { " $addToSet " : { " emails " : " joe@gmail.com " }})
> db.users.findOne({ " _id " : ObjectId( " 4b2d75476cc613d5ee930164 " )})
{
" _id " : ObjectId( " 4b2d75476cc613d5ee930164 " ),
" username " : " joe " ,
" emails " : [
" joe@example.com " ,
" joe@gmail.com " ,
" joe@yahoo.com " ,
]
}
> db.users.update({ " _id " : ObjectId( " 4b2d75476cc613d5ee930164 " )},
... { " $addToSet " : { " emails " : " joe@hotmail.com " }})
> db.users.findOne({ " _id " : ObjectId( " 4b2d75476cc613d5ee930164 " )})
{
" _id " : ObjectId( " 4b2d75476cc613d5ee930164 " ),
" username " : " joe " ,
" emails " : [
" joe@example.com " ,
" joe@gmail.com " ,
" joe@yahoo.com " ,
" joe@hotmail.com "
]
}
> db.users.update({ " _id " : ObjectId( " 4b2d75476cc613d5ee930164 " )}, { " $addToSet " :
... { " emails " : { " $each " : [ " joe@php.net " , " joe@example.com " , " joe@python.org " ]}}})
> db.users.findOne({ " _id " : ObjectId( " 4b2d75476cc613d5ee930164 " )})
{
" _id " : ObjectId( " 4b2d75476cc613d5ee930164 " ),
" username " : " joe " ,
" emails " : [
" joe@example.com " ,
" joe@gmail.com " ,
" joe@yahoo.com " ,
" joe@hotmail.com "
" joe@php.net "
" joe@python.org "
]
}
- 數組修飾符$pop
$pop修飾符從數組的兩端移除一個元素,{$pop : {key : 1}}從數組末端刪除一個元素,{$pop :{key : -1}}從數組起始端刪除一個元素
- 數組修飾符$pull
$pull從數組里移除符合條件的元素
比如,我們有一個待做事項列表
> db.lists.insert({ " todo " : [ " dishes " , " laundry " , " dry cleaning " ]})
> db.lists.update({}, { " $pull " : { " todo " : " laundry " }})
> db.lists.find()
{
" _id " : ObjectId( " 4b2d75476cc613d5ee930164 " ),
" todo " : [
" dishes " ,
" dry cleaning "
]
}
- 按照位置操作數組的值
有兩種方式:一是按照位置,二是使用位置操作符($符號)
我們先看第一種用法,位置是從0開始索引的,我們可以使用這個索引就好像它是數組的一個屬性一樣
假設我們有一篇博客,帶有一些評論
> db.blog.posts.findOne()
{
" _id " : ObjectId( " 4b329a216cc613d5ee930192 " ),
" content " : " ... " ,
" comments " : [
{
" comment " : " good post " ,
" author " : " John " ,
" votes " : 0
},
{
" comment " : " i thought it was too short " ,
" author " : " Claire " ,
" votes " : 3
},
{
" comment " : " free watches " ,
" author " : " Alice " ,
" votes " : - 1
}
]
}
> db.blog.update({ " post " : post_id},
... { " $inc " : { " comments.0.votes " : 1 }})
$就代表了匹配的元素的索引,如果我們想把評論里叫John的那個改成Jim,就可以這樣子做
db.blog.update({ " comments.author " : " John " },
... { " $set " : { " comments.$.author " : " Jim " }})
4.Upsert
這估計是作者自己造的單詞,指如果存在匹配的document就更新,如果不存在匹配就插入。
將update函數的第三個參數設為true即可,如:
shell的save函數也可以達到同樣的目的,如果存在就更新,如果不存在就插入。
save函數使用一個document做參數,如果document有"_id"鍵就更新,如果沒有就插入。
> x.num = 42
42
> db.foo.save(x)
5.更新多個document
缺省情況下,update函數只更新匹配的第一條記錄,余下的不做改變,要想更新所有的匹配記錄,將update函數的第4個參數設為true
... {$set : {gift : " Happy Birthday! " }}, false , true )
6.返回被更新的document
findAndModify命令的調用比普通的update要慢一些,因為它要等待服務器的響應。
findAndModify命令適合處理隊列,或者其他的原子性的get-and-set式的操作。
假設我們有一個處理流程的collection,需要按一定的順序執行,一個document代表了一個處理流程,如下
" _id " : ObjectId(),
" status " : state,
" priority " : N
}
status是個字符串,可能的值是"Ready","Running","Done".我們需要找到Ready狀態優先級最高的處理流程,處理完成后把狀態設為Done。
我們查詢Ready狀態的所有流程,按優先級排序,把最高的那個標記為Running,然后執行處理流程,結束后把狀態設為Done。
db.processes.update({ " _id " : ps._id}, { " $set " : { " status " : " RUNNING " }})
do_something(ps);
db.processes.update({ " _id " : ps._id}, { " $set " : { " status " : " DONE " }})
這個算法并不好,會產生資源競爭。假設我們有兩個線程來處理,一個線程(線程A)獲取了document,另一個線程(線程B)可能在A將狀態設置為 Running之前獲取同一個document,然后兩個線程會執行同一個處理流程。我們可以將檢查status作為update的一部分來避免這個問題,不過會變得復雜:
while ((ps = cursor.next()) != null ) {
ps.update({ " _id " : ps._id, " status " : " READY " },
{ " $set " : { " status " : " RUNNING " }});
var lastOp = db.runCommand({getlasterror : 1 });
if (lastOp.n == 1 ) {
do_something(ps);
db.processes.update({ " _id " : ps._id}, { " $set " : { " status " : " DONE " }})
break ;
}
cursor = db.processes.find({ " status " : " READY " }).sort({ " priority " : - 1 }).limit( 1 );
}
這樣有另外一個問題,依賴于運行時,一個線程可能處理完所有的工作然后結束,而另一個線程無用的跟在后邊。線程A總是能獲取處理流程,線程B試圖獲取同一個處理流程,然后失敗,然后看著A完成所有的工作。這種情況就非常適合使用findAndModify命令,findAndModify命令在同一個操作里返回項目并更新它。
... " query " : { " status " : " READY " },
... " sort " : { " priority " : - 1 },
... " update " : { " $set " : { " status " : " RUNNING " }})
{
" ok " : 1 ,
" value " : {
" _id " : ObjectId( " 4b3e7a18005cab32be6291f7 " ),
" priority " : 1 ,
" status " : " READY "
}
}
Note:返回的document中的狀態仍然是Ready,在修飾符生效之前,document已經返回了。
執行find查看就可以看到status被設置為了Running
{
" _id " : ObjectId( " 4b3e7a18005cab32be6291f7 " ),
" priority " : 1 ,
" status " : " RUNNING "
}
所以我們的程序應該是這個樣子:
... " query " : { " status " : " READY " },
... " sort " : { " priority " : - 1 },
... " update " : { " $set " : { " status " : " RUNNING " }}).value
> do_something(ps)
> db.process.update({ " _id " : ps._id}, { " $set " : { " status " : " DONE " }})
findAndModify命令里含有一個"update"鍵或"remove"鍵,remove表示匹配的document會被從collection里刪除。
findAndModify命令里各個key的值意義如下
- findAndModify: 字符串,collection的名字
- query:查詢document,檢索document的條件
- sort:按照什么排序
- update:修飾符document,如何更新匹配的document
- remove:布爾值,指示是否刪除document
- new:布爾值,指示返回的document是更新前的還是更新后的,缺省為更新前的。
7.密西西比河此岸的最快書寫(The Fastest Write This Side of Mississippi)
本章節所關注的三個操作(insert,update,remove)看起來都是瞬發的,因為它們不會等待服務器的響應。
這并不是異步,應當被看作是"fire-and-forget"型的函數:客戶端向服務器發送了document然后就繼續自己的事情,客戶端從不會收到一個響應諸如“ok,我收到你的消息啦”或者“不ok,你得給我重新發送一次”之類的東西。
安全操作
這些操作的安全版本就是在執行操作之后立刻調用getLastError命令。驅動會等待服務器響應并做相應的處理,通常是拋出一個異常,開發人員可以捕獲然后處理。
操作成功之后,getLastError也會返回一些信息,比如update或remove,信息里包含了受影響的document數。
請求和連接
數據庫為每個到mongoDB的連接建立一個請求隊列,客戶端發出一個請求,就會被放到隊列的尾部。
注意是一個連接一個隊列,如果我們打開兩個shell,那么我們有了兩個連接,如果我們在一個shell里執行插入,然后在另一個shell里執行查詢,有可能得不到剛才插入的document。在同一個shell里執行插入和查詢不會有問題,插入的document會被返回。這種情況在使用 Ruby,Python 和Jave驅動時尤其值得注意,因為它們都是用連接池,出于性能上的考慮,這些驅動打開多個連接,然后將請求分配給它們。不過它們本身都有自己的機制,保證一系列的請求會使用一個連接來處理。