摘要:所以,兩個需要同時進行的更新會迅速接連完成,此過程不會破壞文檔最新的更新會取得勝利。可以使用原子性的更新修改器,指定對文檔中的某些字段進行更新。
上一篇文章:MongoDB指南---5、創(chuàng)建、刪除文檔
下一篇文章:MongoDB指南---7、find簡介與查詢條件
文檔存入數(shù)據(jù)庫以后,就可以使用update方法來更新它。update有兩個參數(shù),一個是查詢文檔,用于定位需要更新的目標文檔;另一個是修改器(modifier)文檔,用于說明要對找到的文檔進行哪些修改。
更新操作是不可分割的:若是兩個更新同時發(fā)生,先到達服務器的先執(zhí)行,接著執(zhí)行另外一個。所以,兩個需要同時進行的更新會迅速接連完成,此過程不會破壞文檔:最新的更新會取得“勝利”。
最簡單的更新就是用一個新文檔完全替換匹配的文檔。這適用于進行大規(guī)模模式遷移的情況。例如,要對下面的用戶文檔做一個比較大的調整:
{ "_id" : ObjectId("4b2b9f67a1f631733d917a7a"), "name" : "joe", "friends" : 32, "enemies" : 2 }
我們希望將"friends"和"enemies"兩個字段移到"relationships"子文檔中。可以在shell中改變文檔的結構,然后使用update替換數(shù)據(jù)庫中的當前文檔:
> var joe = db.users.findOne({"name" : "joe"}); > joe.relationships = {"friends" : joe.friends, "enemies" : joe.enemies}; { "friends" : 32, "enemies" : 2 }> joe.username = joe.name; "joe" > delete joe.friends; true > delete joe.enemies; true > delete joe.name; true > db.users.update({"name" : "joe"}, joe);
現(xiàn)在,用findOne查看更新后的文檔結構。
{ "_id" : ObjectId("4b2b9f67a1f631733d917a7a"), "username" : "joe", "relationships" : { "friends" : 32, "enemies" : 2 } }
一個常見的錯誤是查詢條件匹配到了多個文檔,然后更新時由于第二個參數(shù)的存在就產生重復的"_id"值。數(shù)據(jù)庫會拋出錯誤,任何文檔都不會更新。
例如,有好幾個文檔都有相同的"name"值,但是我們沒有意識到:
> db.people.find() {"_id" : ObjectId("4b2b9f67a1f631733d917a7b"), "name" : "joe", "age" : 65}, {"_id" : ObjectId("4b2b9f67a1f631733d917a7c"), "name" : "joe", "age" : 20}, {"_id" : ObjectId("4b2b9f67a1f631733d917a7d"), "name" : "joe", "age" : 49},·
現(xiàn)在如果第二個Joe過生日,要增加"age"的值,我們可能會這么做:
> 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
到底怎么了?調用update時,數(shù)據(jù)庫會查找一個"name"值為"Joe"的文檔。找到的第一個是65歲的Joe。然后數(shù)據(jù)庫試著用變量joe中的內容替換找到的文檔,但是會發(fā)現(xiàn)集合里面已經(jīng)有一個具有同樣"_id"的文檔。所以,更新就會失敗,因為"_id"值必須唯一。為了避免這種情況,最好確保更新時總是指定一個唯一文檔,例如使用"_id"這樣的鍵來匹配。對于上面的例子,這才是正確的更新方法:
> db.people.update({"_id" : ObjectId("4b2b9f67a1f631733d917a7c")}, joe)
使用"_id"作為查詢條件比使用隨機字段速度更快,因為是通過"_id"建立的索引。第5章會介紹索引對更新和其他操作的影響。
3.3.2 使用修改器通常文檔只會有一部分要更新。可以使用原子性的更新修改器(update modifier),指定對文檔中的某些字段進行更新。更新修改器是種特殊的鍵,用來指定復雜的更新操作,比如修改、增加或者刪除鍵,還可能是操作數(shù)組或者內嵌文檔。
假設要在一個集合中放置網(wǎng)站的分析數(shù)據(jù),只要有人訪問頁面,就增加計數(shù)器。可以使用更新修改器原子性地完成這個增加。每個URL及對應的訪問次數(shù)都以如下方式存儲在文檔中:
{ "_id" : ObjectId("4b253b067525f35f94b60a31"), "url" : "www.example.com", "pageviews" : 52 }
每次有人訪問頁面,就通過URL找到該頁面,并用"$inc"修改器增加"pageviews"的值。
> db.analytics.update({"url" : "www.example.com"}, ... {"$inc" : {"pageviews" : 1}})
現(xiàn)在,執(zhí)行一個find操作,會發(fā)現(xiàn)"pageviews"的值增加了1。
> db.analytics.find() { "_id" : ObjectId("4b253b067525f35f94b60a31"), "url" : "www.example.com", "pageviews" : 53 }
使用修改器時,"_id"的值不能改變。(注意,整個文檔替換時可以改變"_id"。)其他鍵值,包括其他唯一索引的鍵,都是可以更改的。
1. "$set"修改器入門"$set"用來指定一個字段的值。如果這個字段不存在,則創(chuàng)建它。這對更新模式或者增加用戶定義的鍵來說非常方便。例如,用戶資料存儲在下面這樣的文檔里:
> 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"}})
之后文檔就有了"favorite book"鍵。
> 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"甚至可以修改鍵的類型。例如,如果用戶覺得喜歡很多本書,就可以將"favorit ebook"鍵的值變成一個數(shù)組:
> db.users.update({"name" : "joe"}, ... {"$set" : {"favorite book" : ... ["Cat"s Cradle", "Foundation Trilogy", "Ender"s Game"]}})
如果用戶突然發(fā)現(xiàn)自己其實不愛讀書,可以用"$unset"將這個鍵完全刪除:
> db.users.update({"name" : "joe"}, ... {"$unset" : {"favorite book" : 1}})
現(xiàn)在這個文檔就和剛開始時一樣了。
也可以用"$set"修改內嵌文檔:
> db.blog.posts.findOne() { "_id" : ObjectId("4b253b067525f35f94b60a31"), "title" : "A Blog Post", "content" : "...", "author" : { "name" : "joe", "email" : "joe@example.com" } } > db.blog.posts.update({"author.name" : "joe"}, ... {"$set" : {"author.name" : "joe schmoe"}}) > db.blog.posts.findOne() { "_id" : ObjectId("4b253b067525f35f94b60a31"), "title" : "A Blog Post", "content" : "...", "author" : { "name" : "joe schmoe", "email" : "joe@example.com" } }
增加、修改或刪除鍵時,應該使用$修改器。要把"foo"的值設為"bar",常見的錯誤做法如下:
> db.coll.update(criteria, {"foo" : "bar"})
這會事與愿違。實際上這會將整個文檔用{"foo":"bar"}替換掉。一定要使用以$開頭的修改器來修改鍵/值對。
2. 增加和減少"$inc"修改器用來增加已有鍵的值,或者該鍵不存在那就創(chuàng)建一個。對于更新分析數(shù)據(jù)、因果關系、投票或者其他有變化數(shù)值的地方,使用這個都會非常方便。
假如建立了一個游戲集合,將游戲和變化的分數(shù)都存儲在里面。比如用戶玩彈球(pinball)游戲,可以插入一個包含游戲名和玩家的文檔來標識不同的游戲:
> db.games.insert({"game" : "pinball", "user" : "joe"})
要是小球撞到了磚塊,就會給玩家加分。分數(shù)可以隨便給,這里就把玩家得分基數(shù)約定成50好了。使用"$inc"修改器給玩家加50分:
> db.games.update({"game" : "pinball", "user" : "joe"}, ... {"$inc" : {"score" : 50}})
更新后,可以看到:
> db.games.findOne() { "_id" : ObjectId("4b2d75476cc613d5ee930164"), "game" : "pinball", "user" : "joe", "score" : 50 }
分數(shù)(score)鍵原來并不存在,所以"$inc"創(chuàng)建了這個鍵,并把值設定成增加量:50。
如果小球落入加分區(qū),要加10 000分。只要給"$inc"傳遞一個不同的值就好了:
> db.games.update({"game" : "pinball", "user" : "joe"}, ... {"$inc" : {"score" : 10000}})
現(xiàn)在來看看結果:
> db.games.find() { "_id" : ObjectId("4b2d75476cc613d5ee930164"), "game" : "pinball", "user" : "joe", "score" : 10050 }
"score"鍵已經(jīng)有了,而且有一個數(shù)字類型的值,所以服務器就給這個值增加了10 000。
"$inc"與"$set"的用法類似,就是專門來增加(和減少)數(shù)字的。"$inc"只能用于整型、長整型或雙精度浮點型的值。要是用在其他類型的數(shù)據(jù)上就會導致操作失敗,例如null、布爾類型以及數(shù)字構成的字符串,而在其他很多語言中,這些類型都會自動轉換為數(shù)值類型。
> db.foo.insert({"count" : "1"}) > db.foo.update({}, {"$inc" : {"count" : 1}}) Cannot apply $inc modifier to non-number
另外,"$inc"鍵的值必須為數(shù)字。不能使用字符串、數(shù)組或其他非數(shù)字的值。否則就會提示“Modifier"$inc"allowed for numbers only”(修改器"$inc"只允許使用數(shù)值類型)這樣的錯誤。要修改其他類型,應該使用"$set"或者一會兒要講到的數(shù)組修改器。
3. 數(shù)組修改器有一大類很重要的修改器可用于操作數(shù)組。數(shù)組是常用且非常有用的數(shù)據(jù)結構:它們不 僅是可通過索引進行引用的列表,而且還可以作為數(shù)據(jù)集(set)來用。
4. 添加元素如果數(shù)組已經(jīng)存在,"$push"會向已有的數(shù)組末尾加入一個元素,要是沒有就創(chuàng)建一個新的數(shù)組。例如,假設要存儲博客文章,要添加一個用于保存數(shù)組的"comments"(評論)鍵。可以向還不存在的"comments"數(shù)組添加一條評論,這個數(shù)組會被自動創(chuàng)建,并加入一條評論:
> 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." } ] }
要是還想添加一條評論,繼續(xù)使用"$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." } ] }
這是一種比較簡單的"$push"使用形式,也可以將它應用在一些比較復雜的數(shù)組操作中。使用"$each"子操作符,可以通過一次"$push"操作添加多個值。
> db.stock.ticker.update({"_id" : "GOOG"}, ... {"$push" : {"hourly" : {"$each" : [562.776, 562.790, 559.123]}}})
這樣就可以將三個新元素添加到數(shù)組中。如果指定的數(shù)組中只含有一個元素,那這個操作就等同于沒有使用"$each"的普通"$push"操作。
如果希望數(shù)組的最大長度是固定的,那么可以將"$slice"和"$push"組合在一起使用,這樣就可以保證數(shù)組不會超出設定好的最大長度,這實際上就得到了一個最多包含N個元素的數(shù)組:
> db.movies.find({"genre" : "horror"}, ... {"$push" : {"top10" : { ... "$each" : ["Nightmare on Elm Street", "Saw"], ... "$slice" : -10}}})
這個例子會限制數(shù)組只包含最后加入的10個元素。"$slice"的值必須是負整數(shù)。
如果數(shù)組的元素數(shù)量小于10("$push"之后),那么所有元素都會保留。如果數(shù)組的元素數(shù)量大于10,那么只有最后10個元素會保留。因此,"$slice"可以用來在文檔中創(chuàng)建一個隊列。
最后,可以在清理元素之前使用"$sort",只要向數(shù)組中添加子對象就需要清理:
> db.movies.find({"genre" : "horror"}, ... {"$push" : {"top10" : { ... "$each" : [{"name" : "Nightmare on Elm Street", "rating" : 6.6}, ... {"name" : "Saw", "rating" : 4.3}], ... "$slice" : -10, ... "$sort" : {"rating" : -1}}}})
這樣會根據(jù)"rating"字段的值對數(shù)組中的所有對象進行排序,然后保留前10個。注意,不能只將"$slice"或者"$sort"與"$push"配合使用,且必須使用"$each"。
5. 將數(shù)組作為數(shù)據(jù)集使用你可能想將數(shù)組作為集合使用,保證數(shù)組內的元素不會重復。可以在查詢文檔中用"$ne"來實現(xiàn)。例如,要是作者不在引文列表中,就添加進去,可以這么做:
> db.papers.update({"authors cited" : {"$ne" : "Richie"}}, ... {$push : {"authors cited" : "Richie"}})
也可以用"$addToSet"來實現(xiàn),要知道有些情況"$ne"根本行不通,有些時候更適合用"$addToSet"。
例如,有一個表示用戶的文檔,已經(jīng)有了電子郵件地址的數(shù)據(jù)集:
> 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"組合就不能實現(xiàn)。例如,想一次添加多個郵件地址,就可以使用這些修改器:
> 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" ] }6. 刪除元素
有幾個從數(shù)組中刪除元素的方法。若是把數(shù)組看成隊列或者棧,可以用"$pop",這個修改器可以從數(shù)組任何一端刪除元素。{"$pop":{"key":1}}從數(shù)組末尾刪除一個元素,{"$pop":{"key":-1}}則從頭部刪除。
有時需要基于特定條件來刪除元素,而不僅僅是依據(jù)元素位置,這時可以使用"$pull"。例如,有一個無序的待完成事項列表:
> db.lists.insert({"todo" : ["dishes", "laundry", "dry cleaning"]})
要是想把洗衣服(laundry)放到第一位,可以從列表中先把它刪掉:
> db.lists.update({}, {"$pull" : {"todo" : "laundry"}})
通過查找,會發(fā)現(xiàn)只有兩個元素了:
> db.lists.find() { "_id" : ObjectId("4b2d75476cc613d5ee930164"), "todo" : [ "dishes", "dry cleaning" ] }
"$pull"會將所有匹配的文檔刪除,而不是只刪除一個。對數(shù)組[1,1,2,1]執(zhí)行pull 1,結果得到 只有一個元素的數(shù)組2。
數(shù)組操作符只能用于包含數(shù)組值的鍵。例如,不能將一個整數(shù)插入數(shù)組,也不能將一個字符串從數(shù)組中彈出。要修改標量值,使用"$set"或者"$inc"。
若是數(shù)組有多個值,而我們只想對其中的一部分進行操作,就需要一些技巧。有兩種方法操作數(shù)組中的值:通過位置或者定位操作符("$")。
數(shù)組下標都是以0開頭的,可以將下標直接作為鍵來選擇元素。例如,這里有個文檔,其中包含由內嵌文檔組成的數(shù)組,比如包含評論的博客文章。
> 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 } ] }
如果想增加第一個評論的投票數(shù)量,可以這么做:
> db.blog.update({"post" : post_id}, ... {"$inc" : {"comments.0.votes" : 1}})
但是很多情況下,不預先查詢文檔就不能知道要修改的數(shù)組的下標。為了克服這個困難,MongoDB提供了定位操作符"$",用來定位查詢文檔已經(jīng)匹配的數(shù)組元素,并進行更新。例如,要是用戶John把名字改成了Jim,就可以用定位符替換他在評論中的名字:
db.blog.update({"comments.author" : "John"}, ... {"$set" : {"comments.$.author" : "Jim"}})
定位符只更新第一個匹配的元素。所以,如果John發(fā)表了多條評論,那么他的名字只在第一條評論中改變。
8. 修改器速度有的修改器運行比較快。$inc能就地修改,因為不需要改變文檔的大小,只需要將 鍵的值修改一下(對文檔大小的改變非常小),所以非常快。而數(shù)組修改器可能會改變文檔的大小,就會慢一些("$set"能在文檔大小不發(fā)生變化時立即修改它,否則性能也會有所下降)。
將文檔插入到MongoDB中時,依次插入的文檔在磁盤上的位置是相鄰的。因此,如果一個文檔變大了,原先的位置就放不下這個文檔了,這個文檔就會被移動到集合中的另一個位置。
可以在實際操作中看到這種變化。創(chuàng)建一個包含幾個文檔的集合,對某個位于中間的文檔進行修改,使其尺寸變大。然后會發(fā)現(xiàn)這個文檔被移動到了集合的尾部:
> db.coll.insert({"x" :"a"}) > db.coll.insert({"x" :"b"}) > db.coll.insert({"x" :"c"}) > db.coll.find() { "_id" : ObjectId("507c3581d87d6a342e1c81d3"), "x" : "a" } { "_id" : ObjectId("507c3583d87d6a342e1c81d4"), "x" : "b" } { "_id" : ObjectId("507c3585d87d6a342e1c81d5"), "x" : "c" } > db.coll.update({"x" : "b"}, {$set: {"x" : "bbb"}}) > db.coll.find() { "_id" : ObjectId("507c3581d87d6a342e1c81d3"), "x" : "a" } { "_id" : ObjectId("507c3585d87d6a342e1c81d5"), "x" : "c" } { "_id" : ObjectId("507c3583d87d6a342e1c81d4"), "x" : "bbb" }
MongoDB不得不移動一個文檔時,它會修改集合的填充因子(padding factor)。填充因子是MongoDB為每個新文檔預留的增長空間。可以運行db.coll.stats()查看填充因子。執(zhí)行上面的更新之前,"paddingFactor"字段的值是1:根據(jù)實際的文檔大小,為每個新文檔分配精確的空間,不預留任何增長空間,如圖3-1所示。讓其中一個文檔增大之后,再次運行這個命令(如圖3-2所示),會發(fā)現(xiàn)填充因子增加到了1.5:為每個新文檔預留其一半大小的空間作為增長空間,如圖3-2所示。如果隨后的更新導致了更多次的文檔移動,填充因子會持續(xù)變大(雖然不會像第一次移動時的變化那么大)。如果不再有文檔移動,填充因子的值會緩慢降低,如圖3-3所示。
圖3-1 最初,文檔之間沒有多余的空間
圖3-2 如果一個文檔因為體積變大而不得不進行移動,它原先占用的空間就閑置了,而且填充因子會增加
圖3-3 之后插入的新文檔都會擁有填充因子指定大小的增長空間。如果在之后的插入中不再發(fā)生文檔移動,填充因子會逐漸變小
移動文檔是非常慢的。MongoDB必須將文檔原先所占的空間釋放掉,然后將文檔寫入另一片空間。因此,應該盡量讓填充因子的值接近1。無法手動設定填充因子的值(除非是要對集合進行壓縮,參見18.4節(jié)),但是可以設計一種不依賴于文檔、可以任意增長的模式。第8章會詳細介紹模式設計的相關內容。
下面用一個簡單的程序來展示原地更新和文檔移動的速度差別。下面的程序插入了一個只包含一個鍵的文檔,并且對這個鍵的值進行了100 000次增加:
> db.tester.insert({"x" : 1}) > var timeInc = function() { ... var start = (new Date()).getTime(); ... ... for (var i=0; i<100000; i++) { ... db.tester.update({}, {"$inc" : {"x" : 1}}); ... db.getLastError(); ... } ... ... var timeDiff = (new Date()).getTime() - start; ... print("Updates took: "+timeDiff+"ms"); ... } > timeInc()
在MacBook Air上,總共花費了7.33秒。也就是每秒超過13 000次更新。現(xiàn)在,使用"$push"向一個只有一個鍵的數(shù)組中插入新數(shù)據(jù),重復100 000次。將上面例子中用于更新文檔的代碼修改為:
... db.tester.update({}, {"$push" : {"x" : 1}})
這個程序運行時間為67.58秒,每秒少于1500次更新。
使用"$push"以及其他一些數(shù)組修改器是非常好的,而且通常是必要的,但是,在進行類似的更新時,需要好好權衡一下。如果"$push"成為了瓶頸,那么將一個內嵌文檔取出放入一個多帶帶的集合中,手動填充,或者使用第8章將要介紹的其他某項技術,都很值得。
寫作本書時,MongoDB仍然不能很好地重用空白空間,因此頻繁移動文檔會產生大量空的數(shù)據(jù)文件。如果有太多不能重用的空白空間,你會經(jīng)常在日志中看到如下信息:
Thu Apr 5 01:12:28 [conn124727] info DFM::findAll(): extent a:7f18dc00 was empty, skipping ahead
這就是說,執(zhí)行查詢時,MongoDB會在整個范圍(entire extent,可以在附錄B中查看相關定義。簡單來說,它就是集合的一個子集)內進行查找,卻找不到任何文檔:這只是個空白空間。這個消息提示本身沒什么影響,但是它指出你當前擁有太多的碎片,可能需要進行壓縮。
如果你的模式在進行插入和刪除時會進行大量的移動或者是經(jīng)常打亂數(shù)據(jù),可以使用usePowerOf2Sizes選項以提高磁盤復用率。可以通過collMod命令來設定這個選項:
> db.runCommand({"collMod" : collectionName, "usePowerOf2Sizes" : true})
這個集合之后進行的所有空間分配,得到的塊大小都是2的冪。由于這個選項會導致初始空間分配不再那么高效,所以應該只在需要經(jīng)常打亂數(shù)據(jù)的集合上使用。在一個只進行插入或者原地更新的集合上使用這個選項,會導致寫入速度變慢。
如果在這個命令中指定"usePowerOf2Sizes"選項的值為false,就會關閉這種特殊分配機制。這個選項只會影響之后新分配的記錄,因此,在已有的集合上運行這個命令或者是更改這個選項的值,不會對現(xiàn)有數(shù)據(jù)產生影響。
upsert是一種特殊的更新。要是沒有找到符合更新條件的文檔,就會以這個條件和更新文檔為基礎創(chuàng)建一個新的文檔。如果找到了匹配的文檔,則正常更新。upsert非常方便,不必預置集合,同一套代碼既可以用于創(chuàng)建文檔又可以用于更新文檔。
我們回過頭看看那個記錄網(wǎng)站頁面訪問次數(shù)的例子。要是沒有upsert,就得試著查詢URL,沒有找到就得新建一個文檔,找到的話就增加訪問次數(shù)。要是把這個寫成JavaScript程序,會是下面這樣的:
// 檢查這個頁面是否有一個文檔 blog = db.analytics.findOne({url : "/blog"}) // 如果有,就將視圖數(shù)加/并保存 if (blog) { blog.pageviews++; db.analytics.save(blog); } // 否則為這個頁面創(chuàng)建一個新文檔 else { db.analytics.save({url : "/blog", pageviews : 1}) }
這就是說如果有人訪問頁面,我們得先對數(shù)據(jù)庫進行查詢,然后選擇更新或者插入。要是多個進程同時運行這段代碼,還會遇到同時對給定URL插入多個文檔這樣的競態(tài)條件。
要是使用upsert,既可以避免競態(tài)問題,又可以縮減代碼量(update的第3個參數(shù)表示這是個upsert):
db.analytics.update({"url" : "/blog"}, {"$inc" : {"pageviews" : 1}}, true)
這行代碼和之前的代碼作用完全一樣,但它更高效,并且是原子性的!創(chuàng)建新文檔會將條件文檔作為基礎,然后對它應用修改器文檔。
例如,要是執(zhí)行一個匹配鍵并增加對應鍵值的upsert操作,會在匹配的文檔上進行增加:
> db.users.update({"rep" : 25}, {"$inc" : {"rep" : 3}}, true) > db.users.findOne() { "_id" : ObjectId("4b3295f26cc613d5ee93018f"), "rep" : 28 }
upsert創(chuàng)建一個"rep"值為25的文檔,隨后將這個值加3,最后得到"rep"為28的文檔。要是不指定upsert選項,{"rep":25}不會匹配任何文檔,也就不會對集合進行任何更新。
要是再次運行這個upsert(條件為{"rep":25}),還會創(chuàng)建一個新文檔。這是因為沒有文檔滿足匹配條件(唯一一個文檔的"rep"值是28)。
有時,需要在創(chuàng)建文檔的同時創(chuàng)建字段并為它賦值,但是在之后的所有更新操作中,這個字段的值都不再改變。這就是"$setOnInsert"的作用。"$setOnInsert"只會在文檔插入時設置字段的值。因此,實際使用中可以這么做:
> db.users.update({}, {"$setOnInsert" : {"createdAt" : new Date()}}, true) > db.users.findOne() { "_id" : ObjectId("512b8aefae74c67969e404ca"), "createdAt" : ISODate("2013-02-25T16:01:50.742Z") }
如果再次運行這個更新,會匹配到這個已存在的文檔,所以不會再插入文檔,因此"createdAt"字段的值也不會改變:
> db.users.update({}, {"$setOnInsert" : {"createdAt" : new Date()}}, true) > db.users.findOne() { "_id" : ObjectId("512b8aefae74c67969e404ca"), "createdAt" : ISODate("2013-02-25T16:01:50.742Z") }
注意,通常不需要保留"createdAt"這樣的字段,因為ObjectIds里包含了一個用于標明文檔創(chuàng)建時間的時間戳。但是,在預置或者初始化計數(shù)器時,或者是對于不使用ObjectIds的集合來說,"$setOnInsert"是非常有用的。
save shell幫助程序save是一個shell函數(shù),如果文檔不存在,它會自動創(chuàng)建文檔;如果文檔存在,它就更新這個文檔。它只有一個參數(shù):文檔。要是這個文檔含有"_id"鍵,save會調用upsert。否則,會調用insert。如果在Shell中使用這個函數(shù),就可以非常方便地對文檔進行快速修改。
> var x = db.foo.findOne() > x.num = 42 42 > db.foo.save(x)
要是不用save的話,最后一行代碼看起來就會比較繁瑣了,比如db.foo.up date({"_id" : x._id}, x)。
3.3.4 更新多個文檔默認情況下,更新只能對符合匹配條件的第一個文檔執(zhí)行操作。要是有多個文檔符合條件,只有第一個文檔會被更新,其他文檔不會發(fā)生變化。要更新所有匹配的文檔,可以將update的第4個參數(shù)設置為true。
update的行為以后可能會發(fā)生變化(服務器可能默認會更新所有匹配的文檔,只有第4個參數(shù)為false才會只更新一個),所以建議每次都顯式表明要不要做多文檔更新。
這樣不但更明確地指定了update的行為,而且可以在默認行為發(fā)生變化時正常運行。
多文檔更新對模式遷移非常有用,還可以在對特定用戶發(fā)布新功能時使用。例如,要送給在個指定日期過生日的所有用戶一份禮物,就可以使用多文檔更新,將"gift"增加到他們的賬號:
> db.users.update({"birthday" : "10/13/1978"}, ... {"$set" : {"gift" : "Happy Birthday!"}}, false, true)
這樣就給生日為1978年10月13日的所有用戶文檔添加了"gift"鍵。
想要知道多文檔更新到底更新了多少文檔,可以運行getLastError命令(可以理解為“返回最后一次操作的相關信息”)。鍵"n"的值就是被更新文檔的數(shù)量。
> db.count.update({x : 1}, {$inc : {x : 1}}, false, true) > db.runCommand({getLastError : 1}) { "err" : null, "updatedExisting" : true, "n" : 5, "ok" : true }
這里"n"為5,說明有5個文檔被更新了。"updatedExisting"為true,說明是對已有的文檔進行更新。
3.3.5 返回被更新的文檔調用getLastError僅能獲得關于更新的有限信息,并不能返回被更新的文檔。可以通過findAndModify命令得到被更新的文檔。這對于操作隊列以及執(zhí)行其他需要進行原子性取值和賦值的操作來說,十分方便。
假設我們有一個集合,其中包含以一定順序運行的進程。其中每個進程都用如下形式的文檔表示:
{ "_id" : ObjectId(), "status" : state, "priority" : N }
"status"是一個字符串,它的值可以是"READY"、"RUNNING"或"DONE"。需要找到狀態(tài)為"READY"具有最高優(yōu)先級的任務,運行相應的進程函數(shù),然后將其狀態(tài)更新為"DONE"。也可能需要查詢已經(jīng)就緒的進程,按照優(yōu)先級排序,然后將優(yōu)先級最高的進程的狀態(tài)更新為"RUNNING"。完成了以后,就把狀態(tài)改為"DONE"。就像下面這樣:
var cursor = db.processes.find({"status" : "READY"}); ps = cursor.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"}});
這個算法不是很好,可能會導致競態(tài)條件。假設有兩個線程正在運行。A線程讀取了文檔,B線程在A將文檔狀態(tài)改為"RUNNING"之前也讀取了同一個文檔,這樣兩個線程會運行相同的處理過程。雖然可以在更新查詢中進行狀態(tài)檢查來避免這一問題,但是十分復雜:
var cursor = db.processes.find({"status" : "READY"}); cursor.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"}); cursor.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" } }
注意,返回文檔中的狀態(tài)仍然為"READY",因為findAndModify返回的是修改之前的文檔。要是再在集合上進行一次查詢,會發(fā)現(xiàn)這個文檔的"status"已經(jīng)更新成了"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"鍵表示將匹配的文檔從集合里面刪除。例如,現(xiàn)在不用更新狀態(tài)了,而是直接刪掉,就可以像下面這樣:
ps = db.runCommand({"findAndModify" : "processes", "query" : {"status" : "READY"}, "sort" : {"priority" : -1}, "remove" : true}).value do_something(ps)
findAndModify命令有很多可以使用的字段。
findAndModify
字符串,集合名。
query
查詢文檔,用于檢索文檔的條件。
sort
排序結果的條件。
update
修改器文檔,用于對匹配的文檔進行更新(update和remove必須指定一個)。
remove
布爾類型,表示是否刪除文檔(remove和update必須指定一個)。
new
布爾類型,表示返回更新前的文檔還是更新后的文檔。默認是更新前的文檔。
fields
文檔中需要返回的字段(可選)。
upsert
布爾類型,值為true時表示這是一個upsert。默認為false。
"update"和"remove"必須有一個,也只能有一個。要是沒有匹配的文檔,這個命令會返回一個錯誤。
3.4 寫入安全機制寫入安全(Write Concern)是一種客戶端設置,用于控制寫入的安全級別。默認情況下,插入、刪除和更新都會一直等待數(shù)據(jù)庫響應(寫入是否成功),然后才會繼續(xù)執(zhí)行。通常,遇到錯誤時,客戶端會拋出一個異常(有些語言中可能不叫“異常”,不過實質上都是類似的東西)。
有一些選項可以用于精確控制需要應用程序等待的內容。兩種最基本的寫入安全機制是應答式寫入(acknowledged wirte)和非應答式寫入(unacknowledged write)。應答式寫入是默認的方式:數(shù)據(jù)庫會給出響應,告訴你寫入操作是否成功執(zhí)行。非應答式寫入不返回任何響應,所以無法知道寫入是否成功。
通常來說,應用程序應該使用應答式寫入。但是,對于一些不是特別重要的數(shù)據(jù)(比如日志或者是批量加載數(shù)據(jù)),你可能不愿意為了自己不關心的數(shù)據(jù)而等待數(shù)據(jù)庫響應。在這種情況下,可以使用非應答式寫入。
盡管非應答式寫入不返回數(shù)據(jù)庫錯誤,但是這不代表應用程序不需要做錯誤檢查。如果嘗試向已經(jīng)關閉的套接字(socket)執(zhí)行寫入,或者寫入套接字時發(fā)生了錯誤,都會引起異常。
使用非應答式寫入時,一種經(jīng)常被忽視的錯誤是插入無效數(shù)據(jù)。比如,如果試圖插入兩個具有相同"_id"字段的文檔,shell就會拋出異常:
> db.foo.insert({"_id" : 1}) > db.foo.insert({"_id" : 1}) E11000 duplicate key error index: test.foo.$_id_ dup key: { : 1.0 }
如果第二次插入時使用的是非應答式寫入,那么第二次插入就不會拋出異常。鍵重復異常是一種非常常見的錯誤,還有其他很多類似的錯誤,比如無效的修改器或者是磁盤空間不足等。
shell與客戶端程序對非應答式寫入的實際支持并不一樣:shell在執(zhí)行非應答式寫入后,會檢查最后一個操作是否成功,然后才會向用戶輸出提示信息。因此,如果在集合上執(zhí)行了一系列無效操作,最后又執(zhí)行了一個有效操作,shell并不會提示有錯誤發(fā)生。
> db.foo.insert({"_id" : 1}); db.foo.insert({"_id" : 1}); db.foo.count()
可以調用getLastError 手動強制在shell 中進行檢查,這一操作會檢查最后一次 操作中的錯誤。
> db.foo.insert({"_id" : 1}); db.foo.insert({"_id" : 1}); print( ... db.getLastError()); db.foo.count() E11000 duplicate key error index: test.foo.$_id_ dup key: { : 1.0 } 1
編寫需要在shell中執(zhí)行的腳本時,這是非常有用的。
事實上,還有其他一些寫入安全機制,第11章會講述多臺服務器之間的寫入安全,第19章會講述寫入提交。
2012年,默認的寫入安全機制改變了,所以,遺留代碼的行為可能會與預期不一致。在此之前,默認的寫入是非應答式的。
幸好,很容易得知當前代碼是在默認的寫入安全機制發(fā)生變化之前寫的還是之后寫的:默認的寫入機制變?yōu)榘踩珜懭胫螅序寗映绦蚨奸_始使用MongoClient這個類。如果程序使用的連接對象是Mongo或者Connection或者其他內容,那么這段程序使用的就是舊的、默認不安全的API。在默認寫入安全機制發(fā)生變化之前,任何語言都沒有使用MongoClient作為類名,所以,如果你的代碼使用了這個類名,說明你的代碼是寫入安全的。 如果使用的連接不是MongoClient,應在必要時將舊代碼中的非應答式寫入改成應答式寫入.
上一篇文章:MongoDB指南---5、創(chuàng)建、刪除文檔
下一篇文章:MongoDB指南---7、find簡介與查詢條件
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/44012.html
摘要:如將構造函數(shù)作為函數(shù)進行調用即不包括的方式,返回的是日期的字符串表示,而非日期對象。如果不注意這一點,沒有始終使用日期構造函數(shù),將得到一堆混亂的日期對象和日期的字符串。關于日期類的完整解釋,以及構造函數(shù)的參數(shù)格式,參見規(guī)范節(jié)。 上一篇文章:MongoDB指南---2、MongoDB基礎知識-文檔、集合、數(shù)據(jù)庫、客戶端下一篇文章:MongoDB指南---4、MongoDB基礎知識-使用M...
摘要:如將構造函數(shù)作為函數(shù)進行調用即不包括的方式,返回的是日期的字符串表示,而非日期對象。如果不注意這一點,沒有始終使用日期構造函數(shù),將得到一堆混亂的日期對象和日期的字符串。關于日期類的完整解釋,以及構造函數(shù)的參數(shù)格式,參見規(guī)范節(jié)。 上一篇文章:MongoDB指南---2、MongoDB基礎知識-文檔、集合、數(shù)據(jù)庫、客戶端下一篇文章:MongoDB指南---4、MongoDB基礎知識-使用M...
摘要:例如,假設要刪除集合中所有為的人刪除數(shù)據(jù)是永久性的,不能撤銷,也不能恢復。刪除速度刪除文檔通常很快,但是如果要清空整個集合,那么使用直接刪除集合會更快然后在這個空集合上重建各項索引。上一篇文章指南基礎知識使用下一篇文章指南更新文檔 上一篇文章:MongoDB指南---4、MongoDB基礎知識-使用MongoDB Shell下一篇文章:MongoDB指南---6、更新文檔 本章會介紹...
摘要:例如,假設要刪除集合中所有為的人刪除數(shù)據(jù)是永久性的,不能撤銷,也不能恢復。刪除速度刪除文檔通常很快,但是如果要清空整個集合,那么使用直接刪除集合會更快然后在這個空集合上重建各項索引。上一篇文章指南基礎知識使用下一篇文章指南更新文檔 上一篇文章:MongoDB指南---4、MongoDB基礎知識-使用MongoDB Shell下一篇文章:MongoDB指南---6、更新文檔 本章會介紹...
摘要:上一篇文章指南更新文檔下一篇文章指南特定類型的查詢本章將詳細介紹查詢。查詢條件和就是全部的比較操作符,分別對應和。如果查詢優(yōu)化器可以更高效地處理,那就選擇使用它。注意,查詢優(yōu)化器不會對進行優(yōu)化,這與其他操作符不同。 上一篇文章:MongoDB指南---6、更新文檔下一篇文章:MongoDB指南---8、特定類型的查詢 本章將詳細介紹查詢。主要會涵蓋以下幾個方面: 使用find或者f...
摘要:上一篇文章指南更新文檔下一篇文章指南特定類型的查詢本章將詳細介紹查詢。查詢條件和就是全部的比較操作符,分別對應和。如果查詢優(yōu)化器可以更高效地處理,那就選擇使用它。注意,查詢優(yōu)化器不會對進行優(yōu)化,這與其他操作符不同。 上一篇文章:MongoDB指南---6、更新文檔下一篇文章:MongoDB指南---8、特定類型的查詢 本章將詳細介紹查詢。主要會涵蓋以下幾個方面: 使用find或者f...
閱讀 2171·2020-06-12 14:26
閱讀 2477·2019-08-29 16:41
閱讀 1884·2019-08-29 15:28
閱讀 2448·2019-08-26 13:43
閱讀 753·2019-08-26 13:37
閱讀 2773·2019-08-23 18:13
閱讀 2791·2019-08-23 15:31
閱讀 1014·2019-08-23 14:10