MongoDB權威指南(2)- 新增、修改、刪除操作

jopen 12年前發布 | 58K 次閱讀 MongoDB NoSQL數據庫

1.插入和保存document

如前所述,向collection插入document使用insert方法

>  db.foo.insert({ " bar "  :  " baz " })

如果document里邊沒有"_id"鍵,"_id"會被自動創建

批量插入

批量插入是一種更高效的方法,傳遞給database一個document的數組,可以一次插入多個document。單個插入的時候,向 database傳送一個document,前邊會附加一個頭部,告訴database在某個collection執行一次插入操作。批量插入只產生一個 TCP請求,意味著不用處理很多請求,同時也省掉了處理頭部的時間。批量插入只能插入到一個collection里邊去。

批量插入只能用于應用程序接口,shell不支持(至少到目前還不支持)。

另外,如果想導入數據(比如說從mysql),不要使用批量插入,使用命令行工具如mongoimport。

2.刪除document

>  db.users.remove()

這個命令會刪除users里邊的所有document。

remove函數可以有一個查詢用document做參數,以刪除符合條件的document。

>  db.mailing.list.remove({ " opt-out "  :  true })

這個命令刪除所有"opt-out"為true的document。

刪除文檔通常是一個非常快的操作,如果想清除整個collection,還有一種更快的方法,使用drop函數然后重建索引。

3.更新document

udpate方法可以攜帶兩個參數:

  • 查詢用document, 用于定位哪些document將會被更新
  • 修飾符document,用于描述如何修改找到的document

更新是原子性操作,先到達服務器的將會被先執行,后到達的會被后執行,所以,后邊的會覆蓋前邊的修改。

document替換

使用一個新的document來替換匹配的,上一篇文章里用的其實就是document替換,如

db.users.update({ " name "  :  " joe " }, joe);

document替換時一個常見的錯誤是當有多個匹配的document時候可能會導致duplicate key錯誤。舉個例子,

假設我們有好幾個名字都叫joe的document,

>  db.people.find()
{
" _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,

>  joe  =  db.people.findOne({ " name "  :  " joe " " age "  :  20 });
{
" _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的時候就可以這樣子做

>  db.analytics.update({ " url "  :  " www.example.com " },
... {
" $inc "  : { " pageviews "  :  1 }})

"$inc"就是個更新修飾符,使用更新修飾符的時候,不能更新"_id"鍵的值。

下邊我們看看常用的更新修飾符

  • $set
    $set修飾符設定指定的key的值,如果key不存在就創建一個
    假設我們有下邊一個用戶檔案
    >  db.users.findOne()
    {
    " _id "  : ObjectId( " 4b253b067525f35f94b60a31 " ),
    " name "  :  " joe " ,
    " age "  :  30 ,
    " sex "  :  " male " ,
    " location "  :  " Wisconsin "
    }
    現在我們想給用戶加一個項目,他喜愛的書籍,我們就可以用$set修飾符來做
    >  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 "
    }
    如果用戶不喜歡這本書了,喜歡另為一本,我們可以再次用$set來修改
    >  db.users.update({ " name "  :  " joe " },
    ... {
    " $set "  : { " favorite book "  :  " green eggs and ham " }})
    $set不僅可以改變值,還可以改變數據類型,如果用戶喜歡的不是一本,而是很多書
    >  db.users.update({ " name "  :  " joe " },
    ... {
    " $set "  : { " favorite book "  :
    ... [
    " cat's cradle " " foundation trilogy " " ender's game " ]}})
    如果用戶現在又覺得,他其實并不喜歡讀書,我們可以用$unset來刪除key
    >  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. "
    }
    ]
    }
    如果想添加另外一篇評論,就再次使用$push
    >  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. "
    }
    ]
    }
    如果想向數組里追加一個數組里沒有的元素,我們可以在查詢document里使用$ne(not equal,第四章查詢里會有這種邏輯修飾符的說明),如:
    >  db.papers.update({ " authors cited "  : { " $ne "  :  " Richie " }},
    ... {$push : {
    " authors cited "  :  " Richie " }})
    數組修飾符$addToSet可以達到相同的效果,假設我們有個用戶document,用戶有好幾個email
    >  db.users.findOne({ " _id "  : ObjectId( " 4b2d75476cc613d5ee930164 " )})
    {
    " _id "  : ObjectId( " 4b2d75476cc613d5ee930164 " ),
    " username "  :  " joe " ,
    " emails "  : [
    " joe@example.com " ,
    " joe@gmail.com " ,
    " joe@yahoo.com "
    ]
    }
    我們可以使用$addToSet防止插入重復的值
    >  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 "
    ]
    }
    $addToSet還可以和$each聯合使用,一次追加多個不重復的值,這個是$ne/$push組合做不到的
    >  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 " ]})
    我們想把laundry刪掉
    >  db.lists.update({}, { " $pull "  : { " todo "  :  " laundry " }})
    這樣數組里就剩兩項了
    >  db.lists.find()
    {
    " _id "  : ObjectId( " 4b2d75476cc613d5ee930164 " ),
    " todo "  : [
    " dishes " ,
    " dry cleaning "
    ]
    }
    $pull會刪除所有的匹配元素

  • 按照位置操作數組的值
    有兩種方式:一是按照位置,二是使用位置操作符($符號)
    我們先看第一種用法,位置是從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
    }
    ]
    }
    我想給第一篇評論的投票數加1,我們就可以這樣做
    >  db.blog.update({ " post "  : post_id},
    ... {
    " $inc "  : { " comments.0.votes "  :  1 }})
    實際上很多時候我們根本不知道這個索引是多少,我們只知道有這么個匹配的document在,我們可以使用第二種用法,使用位置操作符$,
    $就代表了匹配的元素的索引,如果我們想把評論里叫John的那個改成Jim,就可以這樣子做
    db.blog.update({ " comments.author "  :  " John " },
    ... {
    " $set "  : { " comments.$.author "  :  " Jim " }})

4.Upsert

這估計是作者自己造的單詞,指如果存在匹配的document就更新,如果不存在匹配就插入。

將update函數的第三個參數設為true即可,如:

db.analytics.update({ " url "  :  " /blog " }, { " $inc "  : { " visits "  :  1 }},  true )

shell的save函數也可以達到同樣的目的,如果存在就更新,如果不存在就插入。

save函數使用一個document做參數,如果document有"_id"鍵就更新,如果沒有就插入。

>   var  x  =  db.foo.findOne()
>  x.num  =   42
42
>  db.foo.save(x)

5.更新多個document

缺省情況下,update函數只更新匹配的第一條記錄,余下的不做改變,要想更新所有的匹配記錄,將update函數的第4個參數設為true

>  db.users.update({birthday :  " 10/13/1978 " },
... {$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。

ps  =  db.processes.find({ " status "  :  " READY " ).sort({ " priority "  :  - 1 }).limit( 1 ).next()
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的一部分來避免這個問題,不過會變得復雜:

var  cursor  =  db.processes.find({ " status "  :  " READY " }).sort({ " priority "  :  - 1 }).limit( 1 );
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命令在同一個操作里返回項目并更新它。

>  ps  =  db.runCommand({ " findAndModify "  :  " processes " ,
... 
" 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

>  db.processes.findOne({ " _id "  : ps.value._id})
{
" _id "  : ObjectId( " 4b3e7a18005cab32be6291f7 " ),
" priority "  :  1 ,
" status "  :  " RUNNING "
}

所以我們的程序應該是這個樣子:

>  ps  =  db.runCommand({ " findAndModify "  :  " processes " ,
... 
" 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驅動時尤其值得注意,因為它們都是用連接池,出于性能上的考慮,這些驅動打開多個連接,然后將請求分配給它們。不過它們本身都有自己的機制,保證一系列的請求會使用一個連接來處理。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!