從2012年初開始粥喜,公司的一些核心產(chǎn)品準備開始陸續(xù)遷移到MongoDB上凸主,我們嘗試著從一個小產(chǎn)品開始使用,陸續(xù)將其他產(chǎn)品遷入额湘,到13年年底卿吐,公司產(chǎn)品在數(shù)據(jù)庫選擇上基本實現(xiàn)了NoSQL化旁舰,除了一些事務性要求較高(如支付)的模塊繼續(xù)停留在Mysql上,基本上現(xiàn)在大家都會偏向于使用MongoDB嗡官。就我個人而言箭窜,我覺得一些小項目(如后臺管理),或者需求變化極快的項目(現(xiàn)今的大部分中小移動互聯(lián)網(wǎng)產(chǎn)品)衍腥,如果對于并發(fā)要求不高绽快,沒有特別強的事務性,業(yè)務相對簡單紧阔,基本上就是一到兩人完成的小應用,mongodb應該是是這類應用的首選數(shù)據(jù)庫续担,我自己的體驗的理由如下:
相對于MySQL等關(guān)系型數(shù)據(jù)庫擅耽,mongodb更為輕量,安裝物遇,使用乖仇,部署都輕便得多。
mongodb 的驅(qū)動寫得極為成熟询兴,天然的Bson數(shù)據(jù)結(jié)構(gòu)乃沙,使得存取數(shù)據(jù)都以Map結(jié)構(gòu)進行交互,數(shù)據(jù)接口非常方便诗舰,不需要額外進行數(shù)據(jù)轉(zhuǎn)換警儒,開發(fā)效率明顯提升。較為明顯的對比就是:如果使用MySQL眶根,往往需要使用一個第三方ORM框架進行DB層的操作蜀铲,以及Bean映射和數(shù)據(jù)轉(zhuǎn)換,mongodb完全不需要ORM属百,原生驅(qū)動已經(jīng)做得非常棒了记劝。
相對松散的數(shù)據(jù)庫設計模式,使得它能更好的適應快速變化的需求族扰。當然這一點并不是說用mongodb不需要進行嚴謹?shù)臄?shù)據(jù)庫結(jié)構(gòu)設計了厌丑,只是說在需求變更涉及到庫表修改的時候,不像MySQL那么糾結(jié)要先去弄一下表結(jié)構(gòu)渔呵,我才敢部署應用怒竿。mongodb基本上沒有這個痛感。
mongodb 現(xiàn)在的最新穩(wěn)定版是2.4.8厘肮,至此愧口,它提供了相對完善的操作API,而且把Aggregation框架加入以后类茂,原來一直頭痛的各種統(tǒng)計操作也有了較好的解決方案耍属,現(xiàn)在可以比較放心的說托嚣,MySQL能完成的幾乎所有事情,mongodb都能完成厚骗。
mongodb的文檔現(xiàn)在真的好的令人發(fā)指啊示启,應該可以算是業(yè)界文檔的模范了。
這篇文章主要想介紹一下mongodb的一些基本常用的操作领舰,順便將一些工作中的處理和理解也提出來夫嗓,希望能稱得上是一篇進階之作。
1. insert,插入數(shù)據(jù)
insert操作比較簡單冲秽,mongodb提供了insert, save 方法進行數(shù)據(jù)插入操作舍咖。
insert就是普通插入,如果待插入的數(shù)據(jù)中未含有key:'_d'锉桑,mongodb則會自動生成一個類型為ObjectId排霉,key為'_id'的數(shù)據(jù)作為該條記錄的主鍵,如果已經(jīng)含有民轴,則只校驗一下'_id'是否存在于集合中攻柠,未存在則會插入成功,否則會返回一個錯誤后裸。
sava 方法會根據(jù)待處理的數(shù)據(jù)中是否含有key:'_id'進行處理瑰钮,沒有包含則插入數(shù)據(jù),包含則根據(jù)這個_id更新原有數(shù)據(jù)微驶。
另外浪谴,insert方法還可以進行批量操作,只要將需要插入的數(shù)據(jù)按照數(shù)組格式組裝傳入即可祈搜。
基本語法如下:
//
db.collection.insert({key:value});
db.collection.insert([{key:value},{key:value}...]);
db.collection.save({key:value});
2. remove较店,刪除數(shù)據(jù)
remove操作也很簡單,只需要把刪除條件傳入即可容燕。
基本語法:
db.collection.remove({key:value});
如果沒有傳入任何刪除條件梁呈,則會刪除整個集合。
3. update蘸秘,更新數(shù)據(jù)
update稍微復雜一些官卡,我們在開發(fā)中碰到的關(guān)于更新的操作大概有以下三種情況:
- 普通更新操作(update.$set|$unset)。
- 原子更新操作(update.$inc)醋虏。
- 阻塞查詢更新操作(findAndModify.$set|$inc)寻咒。
- 數(shù)組相關(guān)更新操作($push|$pull|$addToSet|$pop 等)。
3.1 普通更新操作
首先來說一下update的基本語法:
db.collection.update( <query>, <update>, <upsert>, <multi> )
query:更新的查詢條件.
update: 更新的數(shù)據(jù).
upsert: 當查詢條件沒有找到數(shù)據(jù)時是否插入,默認false.
multi:是否更新多條颈嚼,默認false.
這里需要強調(diào)一下的是對于選項【update】的處理毛秘,如果是更新全文檔,則無需特別處理;如果只更新文檔中的幾個字段叫挟,則需要加"$set"進行處理艰匙,不然會將文檔覆蓋掉,在寫數(shù)據(jù)處理腳本的時候要特別注意這些地方抹恳。這里提供一個對于【普通更新操作】的示例:
db.Student.update(
{_id:ObjectId("52e8fce17ee72c8860511af6")},
{"$set":{"name":"jay","status":1}},
false,
true
)
3.2 原子更新操作
mongodb對于自增長的處理是通過$inc來實現(xiàn)的员凝,自增長的過程是原子性的。示例如下:
db.Student.update(
{_id:ObjectId("52e8fce17ee72c8860511af6")},
{"$inc":{"age":3}},
false,
true
)
上面這段代碼將Student中的一條記錄的age字段自增長了3奋献。
如果在一個update操作中健霹,我既有更新部分數(shù)據(jù)的需求,又希望對某個字段進行自增長操作,還希望刪除某個字段瓶蚂,這里的處理就很簡單了:
db.Student.update(
{_id:ObjectId("52e8fce17ee72c8860511af6")},
{
"$set":{"name":"jay","status":1}
,"$inc":{"age":3},
,"$unset":{"sex":1}
},
false,
true
)
3.3 阻塞查詢更新操作
這里需要提一下mongodb的鎖機制了糖埋。
3.3.1 MongoDB 使用的鎖
MongoDB 使用的是“readers-writer”鎖, 可以支持并發(fā)但有很大的局限性窃这,當一個讀鎖存在,許多讀操作可以使用這把鎖阶捆,然而, 當一個寫鎖的存在,一個單一的寫操作會 exclusively 持有該鎖钦听,同時其它讀,寫操作不能使用共享這個鎖倍奢;舉個例子朴上,假設一個集合里有 10 個文檔,多個 update 操作不能并發(fā)在這個集合上卒煞,即使是更新不同的文檔痪宰。
3.3.2鎖的粒度
在2.2版本以前,mongod只有全局鎖畔裕;在2.2版本開始衣撬,大部分讀寫操作只鎖一個庫,相對之前版本扮饶,這個粒度已經(jīng)下降具练,例如如果一個 mongod 實例上有5個庫,如果只對一個庫中的一個集合執(zhí)行寫操作甜无,那么在寫操作過程中扛点,這個庫被鎖;而其它5個庫不影響岂丘。相比RDBMS來說陵究,這個粒度已經(jīng)算很大了!
可以看出奥帘,mongodb這種鎖機制設計得不是很合理铜邮,數(shù)據(jù)到了一定數(shù)量級比較容易出現(xiàn)性能問題,所以要特別注意【更新】和【查詢】操作。
我現(xiàn)在的需求是松蒜,要在mongodb中獲取自增長的Integer類型的主鍵扔茅。利用findAndModify以及mongodb的鎖機制可以實現(xiàn)這一需求。findAndModify既是read的操作牍鞠,又是write的操作咖摹,在執(zhí)行findAndModify時,mongodb會對集合進行writer加鎖难述,其他線程不能進行write操作萤晴,操作完畢以后,它同時返回操作后的最新結(jié)果胁后,保證read的準確性店读。這樣就保證了每一次只能執(zhí)行write and read in document的事情。
我們在實踐中的設計是這么做的:
- 設計一個Collection攀芯,集合名為AutoIds.插入一條數(shù)據(jù):{_id:1}.
- 實現(xiàn)生成自增長并返回主鍵邏輯,這里用的是java驅(qū)動:
public Integer getNextId(String fieldName) {
DBCollection autoIdsColl = db.getAutoIdsCollection();
// _id=1, 確定預先插入的唯一一條記錄
DBObject query = new BasicDBObject("_id", 1);
// 過濾一下查詢的 field
DBObject fields ={_id:1, fieldName:1};
// 排序
DBObject sort = new BasicDBObject("_id", 1);
// 定義每次自增長幅度為1
update = new BasicDBObject("$inc", new BasicDBObject(fieldName, 1));
// 更新并返回
DBObject obj = autoIdsColl.findAndModify(query, fields, sort, false, update, true, true);
// 返回此次更新的Id值
Integer id = (Integer) obj.get(fieldName);
return id;
}
- 由上一步可知,AutoIds只有一條記錄屯断,理論上可以無限橫向擴展,為多個表維護ID侣诺,只需要傳遞不同的ID的key作為getNextId的參數(shù)即可殖演。
相對于關(guān)系型數(shù)據(jù)庫,mongodb需要繞這么一大圈確實有點說不過去年鸳,而且由于鎖機制的欠缺趴久,性能還差了一大截,不過在實際業(yè)務中搔确,mongodb自帶的ObjectId作為主鍵其實能解決大部分問題彼棍,所以也還算能接受。
3.4 數(shù)組更新操作
數(shù)組相關(guān)的更新操作在大部分情況下和普通更新操作沒有啥特別大的區(qū)別膳算,無非就是加了幾個操作符座硕。但是也有一些棘手的操作,由于不常用涕蜂,每次弄的時候總是要回過頭來翻文檔华匾,所以我這里單獨提一下。
3.4.1 添加一個子項到數(shù)組中
db.Student.update(
{_id:ObjectId("52e8fce17ee72c8860511af6")},
{
"$push":{"courses":{"name":"Math","code":"001"}}
},
false,
true
)
3.4.2 添加多個子項到數(shù)組中
db.Student.update(
{_id:ObjectId("52e8fce17ee72c8860511af6")},
{
"$addToSet":{"courses":
{"$each":[ {"name":"Math","code":"001"}
,{"name":"English","code":"002"}
]
}
}
},
false,
true
)
這里的$addToSet會保證帶插入的數(shù)組中相同子項只會存在一個机隙,重復的子項也只會插入一次瘦真。如果業(yè)務需求沒有這么嚴謹,也可以用$push代替黍瞧。
3.4.2 移除指定子項
db.Student.update(
{_id:ObjectId("52e8fce17ee72c8860511af6")},
{
"$pull":{"courses":{"name":"Math","code":"001"}}
},
false,
true
)
3.4.2 更新數(shù)組子項中的某個field
這里要借用占位符 $ 來完成诸尽。 先看示例:
db.Student.update(
{
_id:ObjectId("52e8fce17ee72c8860511af6")
,"courses.code":"001"
},
{
"$set":{"courses.$.name":"MATH"}
},
false,
true
)
這個語句稍微解釋一下:
a) 對于更新的查詢條件,務必加 【"courses.code":"001"】這一項印颤,這樣才能定位到數(shù)組中的具體項您机。這里我之前有一個疑惑,就是加不加【"courses.code":"001"】都能查到同一條記錄,為啥一定要加呢际看,主要是為了定位數(shù)組中的子項咸产。
b) 有了 a)的解釋,【"$set":{"courses.$.name":"MATH"}】中的 "$" 的意思就很好理解了仲闽,它就是用來定位數(shù)組子項當前項的脑溢,這兩個寫法缺一不可。
占位符$的使用在涉及到數(shù)組子項的查詢也需要用到赖欣,后面的章節(jié)會說屑彻。
4. query 查詢
查詢操作其實比較簡單了,mongodb提供了大量的操作符來做這個事情顶吮。之前我也說了mongodb的文檔做得非常好社牲,所以一些普通查詢操作,直接翻文檔吧悴了,里面有語法搏恤,實例,非常棒湃交。 鏈接
這里我就不準備把文檔翻譯一遍了熟空,我寫一下在使用過程中一些必要但是稍微繞了一下的處理。
4.1 優(yōu)雅實現(xiàn) between...and
db.Student.find({
"time":
{
"$gt":start,
"$lt":end
}
})
這個結(jié)構(gòu)對我的啟發(fā)就是:我個人認為 $and 基本上是多余的搞莺。
之前用$and實現(xiàn)的方式:
db.Student.find({
"$and":[
{"time":{"$gt":start}}
,{"time":{"$lt":end}}
]
})
這樣一對比痛阻,后者真的笨重而且多余。所以仔細想想腮敌,似乎所有的查詢條件都不需要通過$and這樣通過數(shù)組來實現(xiàn)呀,Map結(jié)構(gòu)本來就支持多鍵存放的嘛俏扩。
4.2 ‘like’ 的新樣子
db.Student.find({
"name":
{
"$regex":"/abc[dD]{1}/"
}
})
正則表達式來實現(xiàn)like的功能糜工,而且更為強大,唯一需要考慮的就是效率問題录淡。這里順帶也把全文搜索也牽出來了捌木,范圍太大了,以后單獨講嫉戚。
4.3 數(shù)組子項的查詢刨裆,中規(guī)中矩的$elemMatch,還是有更方便的寫法?
示例:
db.Student.find({
"courses":{
"$elemMatch":{"code":"001"}
}
});
偶然發(fā)現(xiàn)還有一個超級簡單的寫法:
db.Student.find({
"courses.code":"001"
});
這里很容易引起混淆彬檀,到底Student的數(shù)據(jù)結(jié)構(gòu)是怎么樣的帆啃?【courses】這個字段類型是Map子文檔(map)還是數(shù)組子文檔(List)呢? 實際上只要它是二者中的任何一種窍帝,都可以用上面的寫法查詢出來努潘。
4.4 根據(jù)數(shù)組子項查詢,希望只返回查詢到的數(shù)組子項,應該怎么寫疯坤?
db.students.find(
{_id:ObjectId("6718703038737487484498")
, "courses.code": "001"
},
{ "courses.$": 1 })
這里find方法使用了第二個參數(shù)报慕,【courses.$】又看到了熟悉的占位符了,這里的作用還是一樣压怠,就是定位到query參數(shù)中查詢到的子項眠冈,并只返回這個子項。
其實查詢操作還有很多地方?jīng)]有說到菌瘫,例如基于位置的查詢蜗顽,全文搜索等。但是只要了解了本文所說的篇幅突梦,日常開發(fā)中應該大部分也夠了诫舅。
查詢操作避不開的話題就是效率問題,我會單獨寫一篇這方面的文章宫患,從索引刊懈,鎖機制等探討一下在mongodb中查詢和更新等操作需要注意的問題。
綜上娃闲,基本的操作都說了一下虚汛,我覺得還是多翻文檔,用多了自然就熟了皇帮。