上一篇文章: MongoDB指南---七、find簡介與查詢條件
下一篇文章: MongoDB指南---九、遊標與數據庫命令
如第2章所述,MongoDB的文檔可使用多種類型的數據。其中有一些在查詢時會有特別的表現。正則表達式
null類型的行爲有點奇怪。它確實能匹配自身,因此要是有一個包含以下文檔的集合:shell
> db.c.find() { "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null } { "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 } { "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }
就能夠按照預期的方式查詢"y"鍵爲null的文檔:數據庫
> db.c.find({"y" : null}) { "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
可是,null不只會匹配某個鍵的值爲null的文檔,並且還會匹配不包含這個鍵的文檔。因此,這種匹配還會返回缺乏這個鍵的全部文檔:segmentfault
> db.c.find({"z" : null}) { "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null } { "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 } { "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }
若是僅想匹配鍵值爲null的文檔,既要檢查該鍵的值是否爲null,還要經過"$exists"條件斷定鍵值已存在:數組
> db.c.find({"z" : {"$in" : [null], "$exists" : true}})
很遺憾,沒有"$eq"操做符,因此這條查詢語句看上去有些使人費解,可是使用只有一個元素的"$in"操做符效果是同樣的。安全
正則表達式可以靈活有效地匹配字符串。例如,想要查找全部名爲Joe或者joe的用戶,就可使用正則表達式執行不區分大小寫的匹配:服務器
> db.users.find({"name" : /joe/i})
系統能夠接受正則表達式標誌(i),但不是必定要有。如今已經匹配了各類大小寫組合形式的joe,若是還但願匹配如"joey"這樣的鍵,能夠略微修改一下剛剛的正則表達式:app
> db.users.find({"name" : /joey?/i})
MongoDB使用Perl兼容的正則表達式(PCRE)庫來匹配正則表達式,任何PCRE支持的正則表達式語法都能被MongoDB接受。建議在查詢中使用正則表達式前,先在JavaScript shell中檢查一下語法,確保匹配與設想的一致。
MongoDB能夠爲前綴型正則表達式(好比/^joey/)查詢建立索引,因此這種類型的查詢會很是高效。
正則表達式也能夠匹配自身。雖然幾乎沒有人直接將正則表達式插入到數據庫中,但要是萬一你這麼作了,也能夠匹配到自身:函數
> db.foo.insert({"bar" : /baz/}) > db.foo.find({"bar" : /baz/}) { "_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "bar" : /baz/ }
查詢數組元素與查詢標量值是同樣的。例如,有一個水果列表,以下所示:工具
> db.food.insert({"fruit" : ["apple", "banana", "peach"]})
下面的查詢:
> db.food.find({"fruit" : "banana"})
會成功匹配該文檔。這個查詢比如咱們對一個這樣的(不合法)文檔進行查詢:{"fruit" : "apple", "fruit" : "banana", "fruit" : "peach"}。
若是須要經過多個元素來匹配數組,就要用"$all"了。這樣就會匹配一組元素。例如,假設建立了一個包含3個元素的集合:
> db.food.insert({"_id" : 1, "fruit" : ["apple", "banana", "peach"]}) > db.food.insert({"_id" : 2, "fruit" : ["apple", "kumquat", "orange"]}) > db.food.insert({"_id" : 3, "fruit" : ["cherry", "banana", "apple"]})
要找到既有"apple"又有"banana"的文檔,可使用"$all"來查詢:
> db.food.find({fruit : {$all : ["apple", "banana"]}}) {"_id" : 1, "fruit" : ["apple", "banana", "peach"]} {"_id" : 3, "fruit" : ["cherry", "banana", "apple"]}
這裏的順序可有可無。注意,第二個結果中"banana"在"apple"以前。要是對只有一個元素的數組使用"$all",就和不用"$all"同樣了。例如,{fruit : {$all : ['apple']}和{fruit : 'apple'}的查詢結果徹底同樣。
也可使用整個數組進行精確匹配。可是,精確匹配對於缺乏元素或者元素冗餘的狀況就不大靈了。例如,下面的方法會匹配以前的第一個文檔:
> db.food.find({"fruit" : ["apple", "banana", "peach"]})
可是下面這個就不會匹配:
> db.food.find({"fruit" : ["apple", "banana"]})
這個也不會匹配:
> db.food.find({"fruit" : ["banana", "apple", "peach"]})
要是想查詢數組特定位置的元素,需使用key.index語法指定下標:
> db.food.find({"fruit.2" : "peach"})
數組下標都是從0開始的,因此上面的表達式會用數組的第3個元素和"peach"進行匹配。
"$size"對於查詢數組來講也是很是有用的,顧名思義,能夠用它查詢特定長度的數組。例如:
> db.food.find({"fruit" : {"$size" : 3}})
獲得一個長度範圍內的文檔是一種常見的查詢。"$size"並不能與其餘查詢條件(好比"$gt")組合使用,可是這種查詢能夠經過在文檔中添加一個"size"鍵的方式來實現。這樣每一次向指定數組添加元素時,同時增長"size"的值。好比,本來這樣的更新:
> db.food.update(criteria, {"$push" : {"fruit" : "strawberry"}})
就要變成下面這樣:
> db.food.update(criteria, ... {"$push" : {"fruit" : "strawberry"}, "$inc" : {"size" : 1}})
自增操做的速度很是快,因此對性能的影響微乎其微。這樣存儲文檔後,就能夠像下面這樣查詢了:
> db.food.find({"size" : {"$gt" : 3}})
很遺憾,這種技巧並不能與"$addToSet"操做符同時使用。
本章前面已經說起,find的第二個參數是可選的,能夠指定須要返回的鍵。這個特別的"$slice"操做符能夠返回某個鍵匹配的數組元素的一個子集。
例如,假設如今有一個博客文章的文檔,咱們但願返回前10條評論,能夠這樣作:
> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : 10}})
也能夠返回後10條評論,只要在查詢條件中使用-10就能夠了:
> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -10}})
"$slice"也能夠指定偏移值以及但願返回的元素數量,來返回元素集合中間位置的某些結果:
db.blog.posts.findOne(criteria, {"comments" : {"$slice" : [23, 10]}})
這個操做會跳過前23個元素,返回第24~33個元素。若是數組不夠33個元素,則返回第23個元素後面的全部元素。
除非特別聲明,不然使用"$slice"時將返回文檔中的全部鍵。別的鍵說明符都是默認不返回未說起的鍵,這點與"$slice"不太同樣。例如,有以下博客文章文檔:
{ "_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." } ] }
用"$slice"來獲取最後一條評論,能夠這樣:
> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -1}}) { "_id" : ObjectId("4b2d75476cc613d5ee930164"), "title" : "A blog post", "content" : "...", "comments" : [ { "name" : "bob", "email" : "bob@example.com", "content" : "good post." } ] }
"title"和"content"都返回了,即使是並無顯式地出如今鍵說明符中。
若是知道元素的下標,那麼"$slice"很是有用。但有時咱們但願返回與查詢條件相匹配的任意一個數組元素。可使用$操做符獲得一個匹配的元素。對於上面的博客文章示例,能夠用以下的方式獲得Bob的評論:
> db.blog.posts.find({"comments.name" : "bob"}, {"comments.$" : 1}) { "_id" : ObjectId("4b2d75476cc613d5ee930164"), "comments" : [ { "name" : "bob", "email" : "bob@example.com", "content" : "good post." } ] }
注意,這樣只會返回第一個匹配的文檔。若是Bob在這篇博客文章下寫過多條評論,只有"comments"數組中的第一條評論會被返回。
文檔中的標量(非數組元素)必須與查詢條件中的每一條語句相匹配。例如,若是使用{"x" : {"$gt" : 10, "$lt" : 20}}進行查詢,只會匹配"x"鍵的值大於等於10而且小於等於20的文檔。可是,假如某個文檔的"x"字段是一個數組,若是"x"鍵的某一個元素與查詢條件的任意一條語句相匹配(查詢條件中的每條語句能夠匹配不一樣的數組元素),那麼這個文檔也會被返回。
下面用一個例子來詳細說明這種狀況。假若有以下所示的文檔:
{"x" : 5} {"x" : 15} {"x" : 25} {"x" : [5, 25]}
若是但願找到"x"鍵的值位於10和20之間的全部文檔,直接想到的查詢方式是使用db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}),但願這個查詢的返回文檔是{"x" : 15}。可是,實際返回了兩個文檔:
> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}) {"x" : 15} {"x" : [5, 25]}
5和25都不位於10和20之間,可是這個文檔也返回了,由於25與查詢條件中的第一個語句(大於10)相匹配,5與查詢條件中的第二個語句(小於20)相匹配。
這使對數組使用範圍查詢沒有用:範圍會匹配任意多元素數組。有幾種方式能夠獲得預期的行爲。
首先,可使用"$elemMatch"要求MongoDB同時使用查詢條件中的兩個語句與一個數組元素進行比較。可是,這裏有一個問題,"$elemMatch"不會匹配非數組元素:
> db.test.find({"x" : {"$elemMatch" : {"$gt" : 10, "$lt" : 20}}) > // 查不到任何結果
{"x" : 15}這個文檔與查詢條件再也不匹配了,由於它的"x"字段是個數組。
若是當前查詢的字段上建立過索引(第5章會講述索引相關內容),可使用min()和max()將查詢條件遍歷的索引範圍限制爲"$gt"和"$lt"的值:
> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}).min({"x" : 10}).max({"x" : 20}) {"x" : 15}
如今,這個查詢只會遍歷值位於10和20之間的索引,再也不與5和25進行比較。只有當前查詢的字段上創建過索引時,纔可使用min()和max(),並且,必須爲這個索引的全部字段指定min()和max()。
在可能包含數組的文檔上應用範圍查詢時,使用min()和max()是很是好的:若是在整個索引範圍內對數組使用"$gt"/"$lt"查詢,效率是很是低的。查詢條件會與全部值進行比較,會查詢每個索引,而不只僅是指定索引範圍內的值。
有兩種方法能夠查詢內嵌文檔:查詢整個文檔,或者只針對其鍵/值對進行查詢。
查詢整個內嵌文檔與普通查詢徹底相同。例如,有以下文檔:
{ "name" : { "first" : "Joe", "last" : "Schmoe" }, "age" : 45 }
要查尋姓名爲Joe Schmoe的人能夠這樣:
> db.people.find({"name" : {"first" : "Joe", "last" : "Schmoe"}})
可是,若是要查詢一個完整的子文檔,那麼子文檔必須精確匹配。若是Joe決定添加一個表明中間名的鍵,這個查詢就再也不可行了,由於查詢條件再也不與整個內嵌文檔相匹配。並且這種查詢仍是與順序相關的,{"last" : "Schmoe","first" : "Joe"}什麼都匹配不到。
若是容許的話,一般只針對內嵌文檔的特定鍵值進行查詢,這是比較好的作法。這樣,即使數據模式改變,也不會致使全部查詢由於要精確匹配而一會兒都掛掉。咱們可使用點表示法查詢內嵌文檔的鍵:
> db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"})
如今,若是Joe增長了更多的鍵,這個查詢依然會匹配他的姓和名。
這種點表示法是查詢文檔區別於其餘文檔的主要特色。查詢文檔能夠包含點來表達「進入內嵌文檔內部」的意思。點表示法也是待插入的文檔不能包含「.」的緣由。將URL做爲鍵保存時常常會遇到此類問題。一種解決方法就是在插入前或者提取後執行一個全局替換,將「.」替換成一個URL中的非法字符。
當文檔結構變得更加複雜之後,內嵌文檔的匹配須要些許技巧。例如,假設有博客文章若干,要找到由Joe發表的5分以上的評論。博客文章的結構以下例所示:
> db.blog.find() { "content" : "...", "comments" : [ { "author" : "joe", "score" : 3, "comment" : "nice post" }, { "author" : "mary", "score" : 6, "comment" : "terrible post" } ] }
不能直接用db.blog.find({"comments" : {"author" : "joe","score" : {"$gte" : 5}}})來查尋。內嵌文檔的匹配,必需要整個文檔徹底匹配,而這個查詢不會匹配"comment"鍵。使用db.blog.find({"comments.author" : "joe","comments.score" : {"$gte" : 5}}也不行,由於符合author條件的評論和符合score條件的評論可能不是同一條評論。也就是說,會返回剛纔顯示的那個文檔。由於"author" : "joe"在第一條評論中匹配了,"score" : 6在第二條評論中匹配了。
要正確地指定一組條件,而沒必要指定每一個鍵,就須要使用"$elemMatch"。這種模糊的命名條件句能用來在查詢條件中部分指定匹配數組中的單個內嵌文檔。因此正確的寫法應該是下面這樣的:
> db.blog.find({"comments" : {"$elemMatch" : {"author" : "joe", "score" : {"$gte" : 5}}}})
"$elemMatch"將限定條件進行分組,僅當須要對一個內嵌文檔的多個鍵操做時纔會用到。
鍵/值對是一種表達能力很是好的查詢方式,可是依然有些需求它沒法表達。其餘方法都敗下陣時,就輪到"$where"子句登場了,用它能夠在查詢中執行任意的JavaScript。這樣就能在查詢中作(幾乎)任何事情。爲安全起見,應該嚴格限制或者消除"$where"語句的使用。應該禁止終端用戶使用任意的"$where"語句。
"$where"語句最多見的應用就是比較文檔中的兩個鍵的值是否相等。假如咱們有以下文檔:
> db.foo.insert({"apple" : 1, "banana" : 6, "peach" : 3}) > db.foo.insert({"apple" : 8, "spinach" : 4, "watermelon" : 4})
咱們但願返回兩個鍵具備相同值的文檔。第二個文檔中,"spinach"和"watermelon"的值相同,因此須要返回該文檔。MongoDB彷佛曆來沒有提供過一個$條件語句來作這種查詢,因此只能用"$where"子句藉助JavaScript來完成了:
> db.foo.find({"$where" : function () { ... for (var current in this) { ... for (var other in this) { ... if (current != other && this[current] == this[other]) { ... return true; ... } ... } ... } ... return false; ... }});
若是函數返回true,文檔就作爲結果集的一部分返回;若是爲false,就不返回。
不是很是必要時,必定要避免使用"$where"查詢,由於它們在速度上要比常規查詢慢不少。每一個文檔都要從BSON轉換成JavaScript對象,而後經過"$where"表達式來運行。並且"$where"語句不能使用索引,因此只在走投無路時才考慮"$where"這種用法。先使用常規查詢進行過濾,而後再使用"$where"語句,這樣組合使用能夠下降性能損失。若是可能的話,使用"$where"語句前應該先使用索引進行過濾,"$where"只用於對結果進行進一步過濾。
進行復雜查詢的另外一種方法是使用聚合工具,第7章會詳細介紹。
在服務器上執行JavaScript時必須注意安全性。若是使用不當,服務器端JavaScript很容易受到注入攻擊,與關係型數據庫中的注入攻擊相似。不過,只要在接受輸入時遵循一些規則,就能夠安全地使用JavaScript。也能夠在運行mongod時指定--noscripting選項,徹底關閉JavaScript的執行。
JavaScript的安全問題都與用戶在服務器上提供的程序相關。若是但願避免這些風險,那麼就要確保不能直接將用戶輸入的內容傳遞給mongod。例如,假如你但願打印一句「Hello, name!」,這裏的name是由用戶提供的。使用以下所示的JavaScript函數是很是容易想到的:
> func = "function() { print('Hello, "+name+"!'); }"
若是這裏的name是一個用戶定義的變量,它可能會是"'); db.dropDatabase();print('"這樣一個字符串,所以,上面的代碼會被轉換成以下代碼:
> func = "function() { print('Hello, '); db.dropDatabase(); print('!'); }"
若是執行這段代碼,你的整個數據庫就會被刪除!
爲了不這種狀況,應該使用做用域來傳遞name的值。以Python爲例:
func = pymongo.code.Code("function() { print('Hello, '+username+'!'); }", {"username": name})
如今,數據庫會輸出以下的內容,不會有任何風險:
Hello, '); db.dropDatabase(); print('!
因爲代碼實際上多是字符串和做用域的混合體,因此大多數驅動程序都有一種特殊類型,用於向數據庫傳遞代碼。做用域是用於表示變量名和值的映射的文檔。對於要被執行的JavaScript函數來講,這個映射就是一個局部做用域。所以,在上面的例子中,函數能夠訪問username這個變量,這個變量的值就是用戶傳進來的字符串。
shell中沒有包含做用域的代碼類型,因此做用域只能在字符串或者JavaScript函數中使用。
上一篇文章: MongoDB指南---七、find簡介與查詢條件
下一篇文章: MongoDB指南---九、遊標與數據庫命令