文檔的一般操作(增刪查改CRUD)
MongoDB自2.6版本起就積極保持文檔中的field的相對順序,而更新某個文檔中的field名稱可能會導致該文檔中的field的重新排序。
write操作包括插入、刪除和更新這三個操作,只負責傳送出命令,而不管命令是否成功傳達(比如突然斷網(wǎng)之類的),即不保證絕對成功操作到數(shù)據(jù)庫,所以被認為是“不安全的操作”。當一條操作指令需要操作到多個文檔時(bulk操作),對每條文檔的操作是原子的,而此條指令引發(fā)的所有操作則不是原子的,但是可以使用$isolated操作符來實現(xiàn)(非shard),存儲引擎在此操作期間會變成單線程以保證原子性。
關于bulk操作有兩種執(zhí)行情況:
- 有序執(zhí)行ordered Bulk Write,速度較慢,如果在執(zhí)行write操作的過程中拋出了error,前面成功的操作并不會rollback。
- 無序執(zhí)行Unordered Bulk Write,如果其中某個write操作出錯了,那么其他的操作將繼續(xù)分別執(zhí)行。
每次的write操作都可能引起所有index的刷新和調(diào)整,這是index給write操作帶來的負面影響,所以并不是越多的索引越好,而是越少越好。
插入 insert
db.collection.insertOne()
插入單個文檔,也可以插入多個文檔(相當bulkWrite用?)。
db.collection.insertMany()
插入多個文檔(起相當bulkWrite作用)。
db.collection.insert()
插入單/多個文檔。
更新 update
db.collection.updateOne()
更新單個文檔。
db.collection.updateMany()
更新多個文檔。
db.collection.replaceOne()
替換單個文檔。
db.collection.update()
多功能更新工具。
db.collection.findOneAndUpdate()
單文檔更新的加強版。
db.collection.findOneAndReplace()
單文檔替換的加強版。
刪除 delete
db.collection.deleteOne()
刪除單個文檔
db.collection.deleteMany()
刪除多個文檔
db.collection.remove()
刪除單/多個文檔,
db.collection.findOneAndDelete()
刪除單個文檔的加強版。
額外的操作
db.collection.save()
插入和修改的巧妙結合。
db.collection.bulkWrite()
提供一個操作集合,可以集insertOne、replaceOne、updateOne、updateMany、deleteOne、deleteMany等一系列操作為一身,有序或無序地執(zhí)行。每單個操作都是原子,而整個bulkWrite操作就不是了。
稍微解釋一下基本的操作
- insert()
功能:在某個集合中插入文檔。
格式:db.collection.insert({field:value, ...})
解釋:參數(shù)只有一個,就是文檔{}。
注意:_id鍵值會自動產(chǎn)生,是一個12字節(jié)的Objectid對象,一般由客戶端產(chǎn)生,如果沒有則服務端產(chǎn)生,有復雜的產(chǎn)生規(guī)則。還可以插入多個文檔,當writeMany用。
例子:db.test.insert({"name":"joe", "age": 18})
- update()
功能:搜索出特定的文檔,并更新其中的任意部分field元素或整個文檔。是原子操作。
格式:db.collect.update( <query>, <update>, { upsert: <boolean>, multi: <boolean>, writeConcern: <document> })
解釋:第3個參數(shù)是可選的,若無則默認不使用。首參數(shù)是查詢條件。同find(),次參數(shù)是更新項,有多種寫法,如$set等。
第3個參數(shù)的upsert項為true:若無匹配<query>,則插入該新文檔。若使用了update operator則有會創(chuàng)建新文檔,再update它。
第3個參數(shù)的multi項為true:可以使用<query>匹配到所有的文檔。
第3個參數(shù)的writeConcern用于感知本次操作的結果,詳看官方文檔。
注意:小心第2個參數(shù)<update>的寫法可能替換整個文檔,比如下面例子會替換整個文檔,除了id。
例子:db.people.update( { name: "Andy" }, { name: "Andy", rating: 1, score: 1 }, { upsert: true })
- save()
功能:類似insert+update的智能結合,憑借_id參數(shù)決定使用哪個,會返回wtriteResult。
格式:db.collection.save(<document>, {writeConcern: <document>})
解釋:尾參數(shù)可選,一般不用。若無_id項,則調(diào)用insert。否則調(diào)用upsert。
注意:_id項一般由client產(chǎn)生。此函數(shù)是可代替的。
例子:db.products.save( { "item": "book", "price": 40 } )
- findAndModify()
功能:默認先返回查詢的結果,再從數(shù)據(jù)庫中修改。返回的對象是未修改過的對象。
格式:db.collection.findAndModify({
query: <document>, 可選
sort: <document>, 可選
remove: <boolean>, 必要
update: <document>, 必要
new: <boolean>, 可選
fields: <document>, 可選
upsert: <boolean>, 可選
bypassDocumentValidation: <boolean>, 可選
writeConcern: <document> 可選
});
解釋:可選參數(shù)只要按需提供即可,而必須有remove或update中的一個。速度較慢。
注意:返回的對象已和數(shù)據(jù)庫中的對象可能已經(jīng)不同了,取決于new。無匹配文檔則返回NULL。
例子:db.people.findAndModify( { query: { state: "active" }, sort: { rating: 1 }, remove: true })。
- find()
功能
:查找指定的文檔,返回所有符合條件的文檔的cursor,可指定返回文檔的field。
格式
:find({field:value}, { field1: <boolean>, field2: <boolean> ... } )
解釋
:第2個參數(shù)是可選的,默認返回整個文檔。若指定了域則返回_id域和指定field組成的文檔。
注意
:_id項是默認返回的,除非指定為0。第2個參數(shù)中還可以不顯示指定的field。首參可以指定多個field, 選中同時匹配者。在mongo shell中默認只是顯示20條文檔,但是也可以將find結果賦值給一個變量,再進行操作的話就可以操作多個選中的文檔。因為cursor是可迭代的。MongoDB3.2文檔提到,在10分鐘內(nèi)若cursor無活動則默認自動關閉,之后此cursor就讀不了文檔了,但是可以提前使用cursor.addOption(DBQuery.Option.noTimeout)
來關閉默認的超時設置,具體需查看相關driver。
這里還有個存在的問題,如果使用find之后,數(shù)據(jù)庫中對應的文檔已經(jīng)被修改了,怎么辦?在mongodb3.2中已經(jīng)改變了默認的storage engine MMAPv1 Storage Engine,而是默認使用WiredTiger,具體可查看Default Storage Engine Change。對于MMAPv1來說,如果在此時修改文檔,可能會導致同一個文檔在一個cursor中返回多次。要解決這個問題,可以查看snapshot mode。
例子1
:db.products.find( { "price": 25}, { "name": 1, "number": 1 } ).addOption(DBQuery.noTimeout)
例子2
:db.products.find( { "price": 25}, { "name": 1, "number": 1 } )
輔助操作
限制
limit() 注:指定返回的文檔數(shù)量。
忽略
skip() 注:忽略前/后n個文檔。
排序
sort() 注:根據(jù)各種條件來排序。
- limit()
功能
:截取前/后n個文檔,用于限制返回文檔的數(shù)量,一般搭配find使用。
格式
:db.students.find().limit( n )
解釋
:n是一個32位有符號整數(shù)。可搭配其他函數(shù)來使用。
注意
:.limit(n)之前必須是一個cursor。 - skip()
功能
:忽略前/后n個文檔,用于挑選返回的文檔,一般搭配find使用。
格式
:db.students.find().skip( n )
解釋
:n是一個32位有符號整數(shù)。可搭配其他函數(shù)來使用。|n|過大會導致效率的下降。 - sort()
功能
:指定排序規(guī)則(即指定field)并對文檔進行排序,一般搭配find使用。
格式
:db.students.find().sort( {field1: 1, field2: -1, ... } )
解釋
:使用1和-1來標識升/降序。limit和skip和sort可以搭配使用,且必須在cursor返回之前使用。BSON類型有默認的比較順序。與sort相關的操作還很多,請參考官方文檔。
特定于類型的查詢
null類型
- 一般情況下,你可以這么使用
db.collection.find({"num": null})
,這樣就能將num=null的文檔都選中了。但是要小心,其他沒有num域的文檔也會被匹配中了,也就是說null還能匹配“不存在的”。 - 如果非要匹配那些num域為null的文檔呢?要先用$exists判斷該域是否存在,若存在,再進行匹配。一般情況下可以這么做
db.collection.find({"num": { $in : [null] , $exists : true } })
或者使用$eq來代替$in也是可以的。
正則表達式
- 有了正則表達式才是“如虎添翼”地匹配吖。
比如想忽略大小寫,有標識i呀:db.test.find({"name": /joe/i})
- MOngoDb使用Perl兼容的正則表達式(PCRE)庫來匹配正則的。建議在JS shell中驗證一下想法,確保正確再使用。
- 正則表達式還可以匹配自身,假設你直接將
{"re": /^qq/}
直接存到數(shù)據(jù)庫中,那么使用db.test.find({"re": /^qq/})
也可能選中這個文檔,
內(nèi)嵌數(shù)組array
- 數(shù)組中的每個元素都可以認為是它的值,只要匹配其中的一個元素,就將文檔選中了。比如
db.test.find({"array": "element2"})
- 匹配多個元素,甚至整個array,可以使用$all操作符,并不需要關心順序問題。比如
{ tags : { $all: [ "ssl" , "security" ] } }
就等同于{ $and: [ { tags: "ssl" }, { tags: "security" } ] }
- 匹配整個array,可以直接將整個array寫出來吖。值得注意的是,只有順序也對應上了才會選中。
- 對元素在array中的位置很關心。可以使用
array.index
語法指定下標,比如db.test.find("fruits.2": "apple")
,指定了fruits[2]="apple",注意下標是從0開始的。
內(nèi)嵌文檔
- 查詢內(nèi)嵌文檔。查詢內(nèi)嵌文檔并不同于內(nèi)嵌數(shù)組array一樣,但有幾分相似。
- 查詢方式(1): 完整匹配,即將整個內(nèi)嵌文檔列出來,且順序一致。
db.test.find({ "post": {"author": "joe", "size": 50} })
- 查詢方式(2):使用$elemMatch(query),可以不管次序地匹配部分field。單field就需要用了。
db.scores.find( { results: { $elemMatch: { $gte: 80, $lt: 85 } } })
- 查詢方式(3):使用dot標識,以
內(nèi)嵌文檔名.field
來標識內(nèi)嵌文檔中的一個field,而不管次序。
db.test.find({ "post.author": "joe", "post.size": 50} })
- 注意:
關于讀操作(read operation)
游標 cursor
- 通過find操作所返回的是一個cursor,每次可以從中讀取一條文檔,直到無文檔可讀。當在mongo shell中使用find的時候,如果所選中的文檔超過20條,會默認讀取20條文檔,當然,可以通過
DBQuery.shellBatchSize
來修改這個數(shù)字,參考Executing Queries。 - 默認情況下,假設一個cursor還沒有迭代完畢,而在10分鐘無活動MongoDB就會關閉這個cursor,這個可以通過例如
var myCursor = db.inventory.find().addOption(DBQuery.Option.noTimeout);
來關閉這個規(guī)定。因為cursor中的文檔是分批到達的,比如大多數(shù)情況下第一批是101條文檔或者是僅足夠超過1mb的大小,余下的批次都是以4mb為單位到達的。每當從一個cursor中讀完一批后,客戶端會發(fā)送請求到server,然后接收下一批查詢到的數(shù)據(jù)。要改變batchsize=4mb的情況,可以參考batchSize()和limit()。 - 如果find之后還添加一個無指定index的sort操作,那么server會一次性讀取加載選中的所有文檔進內(nèi)存再進行排序。
- 如果cursor中的文檔還沒有讀取完畢,cursor.next()可以返回下一個文檔。
- 判斷一個cursor是否還有文檔未讀取,可以使用
cursor.hasNext()
。 - 想查看當前批次中還有多少條文檔,參考
objsLeftInBatch()
。 - 當前批次已經(jīng)讀取完畢,會調(diào)用getmore
操作。 - 想查看某個cursor的信息,可以參考 db.serverStatus()
。 - cursor具有如下這些操作:
cursor.batchSize()
設置server每次發(fā)送多少的文檔到客戶端。
cursor.close() 關閉游標。
cursor.comment()
對此次查詢做一些簡短記錄以方便問題追溯。
cursor.count()
改成返回文檔數(shù),而不是返回文檔。
cursor.explain()
調(diào)出查詢結果的信息。
cursor.forEach()
對每個返回的文檔都應用一次。
cursor.hasNext()
判斷cursor是否已經(jīng)讀取完畢。
cursor.hint()
強制指定一個index進行查詢,而不是讓引擎自動選擇。
cursor.itcount()
通過不斷向server發(fā)送消息,在客戶端做文檔數(shù)統(tǒng)計。
cursor.limit()
限制返回的文檔數(shù)。
cursor.map()
對每個文檔都應用到一個函數(shù)中,并將函數(shù)的返回值收集到一個array中。
cursor.maxScan()
當效率比較重要時,指定最大的掃描item/集合/索引數(shù)。
cursor.maxTimeMS()
指定一個游標的不活動存活時間間隔長度。
cursor.max()
指定索引用的field的上界。
cursor.min()
指定索引用的field的下界。
cursor.next()
讀取下一個文檔。
cursor.noCursorTimeout()
告訴server關閉timeout。
cursor.objsLeftInBatch()
獲取客戶端當前batch中還剩下多少文檔未讀取,batch讀完就要發(fā)送消息給server索取文檔。
cursor.pretty()
設置cursor中結果的顯示格式。
cursor.readConcern()
指定一個read concern。
cursor.readPref()
Specifies a read preference to a cursor to control how the client directs queries to a replica set.
cursor.returnKey()
指示讓cursor返回index keys而不是文檔。
cursor.showRecordId()
為每個選中的文檔添加一個內(nèi)部的存儲引擎ID域。
cursor.size()
統(tǒng)計在應用了skip()和limit()之后選中的文檔數(shù)量。
cursor.skip()
跳過find結果的前/后的n條文檔。
cursor.snapshot()
強迫cursor使用的index是_id,這樣就保證了每個文檔只返回一次,即使文檔中途被修改。
cursor.sort() 對選中文檔進行排序。
cursor.tailable() Marks the cursor as tailable. Only valid for cursors over capped collections.
cursor.toArray() 將選中的文檔塞進一個array返回。
Query(查詢)
- 慎用$nin、$ne等操作符,因為它在使用的時候需要進行大量的匹配,有時甚至比直接檢索整個集合的效果更差。
問題
</br>
problem 1:
假設有如下的批量update操作,再假設服務器都完成了所有update中包含的query操作(mongodb僅保證插入document時的原子性),那么就可能會導致插入很多個_id不一樣的document了,因為帶了upsert參數(shù)。
db.people.update(
{ name: "Andy" },
{ name: "Andy", rating: 1, score: 1 },
{ upsert: true }
)
解決方法:可以嘗試為name域設置一個unique index,這樣就只會有一個update能夠成功插入。
problem 2:
假設有如下的update操作,query表達式中帶有_id.uid(有個點),這樣如果查詢失敗了,因為upsert為true,所以插入也失敗了。
db.collection.update(
{ "_id.name": "Robert Frost", "_id.uid": 0 },
{ "categories": ["poet", "playwright"] },
{ upsert: true }
)
解決方法:查詢中帶的_id域不能帶點。