本部分內容包括:
1、學習原生api的必要性;
2、原生api的書寫語法總結;
3、應用場景舉例;
----------------------------------------------------------------------------------------------------------------------------------------------------------
1、學習原生api的必要性
我們對mongodb針對mongoTemplate與原生api進行了簡單的使用,通過對比,可以很直觀的發現mongoTemplate使用更加簡單,可以在不用了解mongodb的查詢api的情況下直接按照sql的查詢邏輯進行書寫,但一切真的那么完美么,顯然不是。原生api對查詢更加的靈活,更重要的是它與通過命令行以及客戶端工具進行查詢時,對查詢條件的書寫是一致的,這在開發及運維的過程中是很重要的。不熟悉mongodb的查詢語法就無法展開運維工作,也很難在開發時對數據庫的數據進行隨時查詢。
2、原生api的語法總結
首先,我們看一下mongodb查詢條件的幾個簡單寫法,畢竟復雜的查詢也是簡單的條件拼接出來的嘛
A、and 查詢
查詢 age <20 and country='china'的用戶
寫法:{"age":{$lt:20},"country":{$eq:'china'}}
當然,這是用工具查的,如果用命令行,應該寫作db.users.find({"age":{$lt:20},"country":{$eq:'china'}}).pretty(),最后的pretty是用來格式化查詢結果的。
基本語法就是{"條件字段1":{運算符:目標值},"條件字段2":{運算符:目標值}},可以連續多個條件;運算符有$eq, $gt, $lt, $lte, $gte, $ne等;
B、or 查詢
查詢 salary > 9999 or length < 1.5
寫法:{$or:[{"salary":{$gt:9999}}, {"length":{$lt:1.5}}]}
字段的條件書寫還是{"條件字段":{運算符:目標值}},區別只是將所有or連接的條件組裝成了一個數組,且在數組的外邊有一個運算符$or;
C、and 跟 or聯合使用
查詢 country=china and (alary > 9999 or length < 1.5)
寫法:{"country":{$eq:'china'}, $or:[{"salary":{$gt:9999}}, {"length":{$lt:1.5}}]}
注意括號的配對,所有查詢條件最外層有花括號{},具體的查詢條件沒有花括號,是直接鍵值對,鍵為字段名,值為一個對象,一般是判斷條件跟目標值;
D、like 查詢
查詢 username like '張%'
寫法:{"username":{$regex:'張.*'}}
注意,這里不是百分號%,而是.*,一個小數點跟一個*號;,匹配是通過正則運算;具體的正則規則比較多,不做贅述;
E、order 排序
查詢 order by age asc, salary desc
寫法:sort({"age":1,'salary':-1})
注意,這里1位升序,-1為降序排列
F、分頁,查詢m到n條
查詢 limit m,(n-m) --mysql語法
寫法:db.xxx.find().skip(m).limit(n-m)
G、group 分組
查詢 select max(salary),avg(length) from users where age>18 gourp by country; 查詢每個國家的最大薪資跟平均身高;
寫法:
db.users.aggregate([ { $match:{ "age":{"$gt":18} } }, { $group:{ "_id":{"country":"$country"}, "avg_salary":{"$max":"$salary"}, "avg_length":{"$avg":"$length"} } } ])
其中,$match為where的過濾條件,排序字段為$group的"_id"字段,如果有多個排序字段,直接在對象中添加即可;
以上即為常見的sql查詢以及對應的mongodb的查詢寫法,雖然語法跟傳統sql不一樣,但基本邏輯並無大的差別,感性認識上,mongodb的語法結構大概就是 字段名:{比較條件:比較值},熟悉了mongodb的基本語法,我們再看幾個典型的使用場景;
3、應用場景舉例
實際使用中,查詢一般是多個條件的組合,查詢結果可能也要作相應處理,這里就會用到聚合操作。
聚合可以理解為就是一個管道,管道里的每一步的輸出都作為下一步的輸入數據:
輸入文檔 ----> 管道操作1 ----> 管道操作2 ----> 管道操作3 ----> 輸出文檔
常用的管道操作:
$project: 投影,指定輸出文檔 中的字段
$match: 過來條件
$limit: 限制返回的文檔數
$skip: 跳過指定數量的文檔
$unwind: 將文檔中的某一個數組類型的字段拆分為多條,每條包含數組中的一個值
$group:將集合中的文檔分組,用於統計結果
$sort:將輸入文檔排序后輸出
應用舉例:
1、假設微博用戶信息存放在mongodb中,要求用戶界面默認顯示3條評論,點擊顯示更多,可以查看接下來的4條記錄;數據格式如下:
數據:
var cang = {"username" : "cang laoshi", "country" : "japan", "address" : {"aCode" : "411000","add" : "東郡"}, "favorites" : { "movies" : ["蜘蛛俠","鋼鐵俠","蝙蝠俠"], "cites" : ["青島","東莞","上海"] }, "salary":NumberDecimal("9999.88"), "comments":[ { "author": "jack1", "content": "jack的評論1", "commentTime": ISODate("2017-12-11T04:26:18.234Z") }, { "author": "jack2", "content": "jack的評論2", "commentTime": ISODate("2017-12-12T04:26:18.234Z") }, { "author": "jack3", "content": "jack的評論3", "commentTime": ISODate("2017-12-13T04:26:18.234Z") }, { "author": "jack4", "content": "jack的評論4", "commentTime": ISODate("2017-12-14T04:26:18.234Z") }, { "author": "jack5", "content": "jack的評論5", "commentTime": ISODate("2017-12-15T04:26:18.234Z") }, { "author": "jack6", "content": "jack的評論6", "commentTime": ISODate("2017-12-16T04:26:18.234Z") }, { "author": "jack7", "content": "jack的評論7", "commentTime": ISODate("2017-12-17T04:26:18.234Z") } ]}; db.users.insert(cang);
應該這么寫:
db.users.find({"username":"cang laoshi"},{"comments":{"$slice":[3,4]},"$elemMatch":""})
解釋一下:find()中兩個參數,第一個為查詢條件,必須有,第二個參數為投影設置,可有可無;投影的意思是從查詢結果中只找我們需要的進行顯示,而把敏感信息隱藏;一般寫法為{字段名:1,字段名2:0} 1的意思為顯示,0為隱藏,需要注意的是寫的時候,
如果多個字段,要么都是1要么都是0,混搭是不行的,mongodb約束如此,沒得解釋;$slice用於返回內容片段而不是全部,后邊[3,4]的意思為從第3個元素(從0開始計數)連續取4個;$elemMatch一般用於內嵌文檔匹配,此處用作投影使用;
需要注意的是,此處如果不使用$elemMatch,而仍需要只有comments投影的效果,在comments跟$slice后添加comments:1是無效的,因為后面的會把前邊的配置覆蓋掉,最終效果是查詢並顯示了所有的comments;避免方式之一是將評論放在comments.list中,也就是將文檔多添加了一層list,可以在設置$slice的時候用comments.list,但這樣會導致文檔結構多了一層無用list,這么設計並不好。
2、數據結構仍然跟上結構類似,但增加幾條數據,然后查詢 tony1或者jack1 評論過的用戶
應該這么寫:db.users.find({"comments.author":{"$in":["tony1","jack1"]}})
當然,這里也可以用or來實現,寫的會略微復雜一些;同理,另一個問題,tony1跟jack1都評論過的用戶應該怎么寫呢?
答案:db.users.find({"comments.author":{"$all":["tony1","jack1"]}})
3、查找jack3的評論為"板凳板凳"的用戶名
db.users.find({"comments":{"$elemMatch":{"author":"jack3","content":"板凳板凳"}}})
4、將1 的場景詳細化:
查看評論,打開頁面默認顯示3條;點擊查看更多,新加載3條;按照評論時間降序排列;
解決辦法之一:
1、查找的時候進行排序,先排序后查找,相對來說是比較麻煩的,但可以在新增的時候就對評論進行排序,那么查詢的時候就無序關心排序問題了;
2、默認3條跟查看更多類似於分頁,在示例1中有;
3、如果有多種排序需求,該如何處理;
按照上面思路,新增評論:
db.users.updateOne({{"username":"name111"},{ "$push":{ "comments":{ "$each":[{ "author":"james", "content":"我的評論aaa", ”commentTime“:ISODate("2019-01-09T04:23:23.233Z") }, { "author":"james2", "content":"我的評論aaa2", ”commentTime“:ISODate("2019-02-09T04:23:23.233Z") }], "$sort":{"commentTime":-1} } } }});
updateOne的兩個參數分別為查詢條件跟更新的數據,$push為新增之意,comments為更新的字段,$each表示所有數據都遵循此操作,$sort為插入時的排序字段,-1表示倒序;
分頁查詢,不再贅述,只提供一種寫法:db.users.find({"username":"cang laoshi"},{"comments":{"$slice":[0,3]},"id":1});會只顯示id跟comments,且comments只有3條;
對於排序,我們的解決思路是插入的時候就進行排序,但這里有個問題,如果我們有多種排序需求,比如按照評論的點贊數進行排序,或者按照作者的知名度(知名度的計算過程不考慮)進行排序,該怎么處理?答案是使用聚合,例如:
db.users.aggregate([ {"$match":{"username":"cang laoshi"}}, {"$unwind":"$comments"}, //將文檔中的某一個數組類型字段拆分為多條,沒條包含數組中的一個值 {"$sort":{"comments.author.xxx":1}}, //排序 {"$project":{"comments":1}}, //投影 {"$skip":0}, {"$limit":3} ])
unwind這個不太好理解,參考文章
https://www.cnblogs.com/wangxiaoheng/articles/9699625.html這個有個例子,說的比較清楚了;
不過多舉例了,通過以上我們發現,mongodb的查詢api還是比較瑣碎的,想要用好的話,還是需要一定練習的。
回想一下mongodb的查詢api,假設有這么個需求,該怎么查:有個訂單表,要求統計2016年5月6號之前,每個用戶每個月消費了多少錢,並按照用戶名排序
答案:
db.orders.aggregate([ {"$match":{"orderTime":{"$lt":new Date("2016-05-06T00:00:00.000Z")}}}, {"$group":{"_id":{"userName":"$useCode","month":{"$month":"$orderTime"}},"total":{"$sum":"$price"}}}, {"$sort":{"total":1}} ])
----------------------------
我們注意到,上邊的查詢示例並沒有join操作,是mongodb不支持關聯查詢么?
這個,倒不是說mongodb不支持關聯查詢,如果要做的話也是可以的,這就是DBRef相關內容,但mongodb的設計理念是盡量使用單表內嵌,而不是表關聯;比如,論壇系統,如果用關系庫來設計,可能要有用戶表,論貼表,評論表等,但用mongodb的話,可能一個用戶表就夠了,發表的帖子,自己的評論等都作為字段內容內嵌到用戶信息里邊即可,這也是mongodb設計的方便之處;但mongodb約束單個文檔內容不能超過16M,當超過16M的時候,需要選擇DBRef,將大數據放到另外一張表中。