03.03 MongoDB極簡教程


1.MongDB 簡介

MongoDB(來自於英文單詞“Humongous”,中文含義為“龐大”)是可以應用於各種規模的企業、各個行業以及各類應用程序的開源數據庫。作為一個適用於敏捷開發的數據庫,MongoDB 的數據模式可以隨著應用程序的發展而靈活地更新。與此同時,它也為開發人員 提供了傳統數據庫的功能:二級索引,完整的查詢系統以及嚴格一致性等等。MongoDB 能夠使企業更加具有敏捷性和可擴展性,各種規模的企業都可以通過使用 MongoDB 來創建新的應用,提高與客戶之間的工作效率,加快產品上市時間,以及降低企業成本。

MongoDB 是專為可擴展性,高性能和高可用性而設計的數據庫。它可以從單服務器部署擴展到大型、複雜的多數據中心架構。利用內存計算的優勢,MongoDB 能夠提供高性能的數據讀寫操作。MongoDB 的本地複製和自動故障轉移功能使您的應用程序具有企業級的可靠性和操作靈活性。

以上內容摘自官網:

1.1 文檔型數據庫

簡而言之,MongoDB是一個免費開源跨平臺的 NoSQL 數據庫,與關係型數據庫不同,MongoDB 的數據以類似於 JSON 格式的二進制文檔存儲:

<code>{
    name: "jack",
    age: 22,
}/<code>

文檔型的數據存儲方式有幾個重要好處:

  1. 文檔的數據類型可以對應到語言的數據類型,如數組類型(Array)和對象類型(Object);
  2. 文檔可以嵌套,有時關係型數據庫涉及幾個表的操作,在 MongoDB 中一次就能完成,可以減少昂貴的連接花銷;
  3. 文檔不對數據結構加以限制,不同的數據結構可以存儲在同一張表;
  4. MongoDB 的文檔數據模型和索引系統能有效提升數據庫性能;
  5. 複製集功能提供數據冗餘,自動化容災容錯,提升數據庫可用性;
  6. 分片技術能夠分散單服務器的讀寫壓力,提高併發能力,提升數據庫的可拓展性;
  7. MongoDB 高性能,高可用性、可擴展性等特點,使其至 2009 年發佈以來,逐漸被認可,並被越來越多的用於生產環境中。AWS、GCP、阿里雲等雲平臺都提供了十分便捷的 MongoDB 雲服務。

1.2 MongoDB 基礎概念

可以使用我們熟悉的 MySQL 數據庫來加以對比:

MySQL 基礎概念MongoDB 對應概念數據庫(database)容器(database)表(table)集合(collection)行(row)文檔(document)列(column)域(filed)索引(index)索引(index)

也借用一下菜鳥教程)的圖來更加形象生動的說明一下:

MongoDB極簡教程

這很容易理解,但是問題在於:我們為什麼要引入新的概念呢?(也就是為什麼我們要把“表”替換成“集合”,“行”替換成“文檔”,“列”替換成“域”呢?)原因在於,其實在 MySQL 這樣的典型關係型數據中,我們是在定義表的時候定義列的,但是由於上述文檔型數據庫的特點,它允許文檔的數據類型可以對應到語言的數據類型,所以我們是在定義文檔的時候才會定義域的。

也就是說,集合中的每個文檔都可以有獨立的域。因此,雖說集合相對於表來說是一個簡化了的容器,而文檔則包含了比行要多得多的信息。

2 搭建環境

怎麼樣都好,搭建好環境就行,這裡以 OS 環境為例,你可以使用 OSX 的 brew 安裝 mongodb:

<code>brew install mongodb/<code>

在運行之前我們需要創建一個數據庫存儲目錄 /data/db:

<code>sudo mkdir -p /data/db/<code>

然後啟動 mongodb,默認數據庫目錄即為 /data/db(如果不是,可以使用 --dbpath 指令來指定):

<code>sudo mongd/<code>

過一會兒你就能看到你的 mongodb 運行起來的提示:

MongoDB極簡教程

具體的搭建過程可以參考菜鳥的教程:http://www.runoob.com/mongodb/mongodb-window-install.html

3 基於 Shell 的 CRUD

3.1 連接實例

通過上面的步驟我們在系統裡運行了一個 mongodb 實例,接下來通過 mongo 命令來連接它:

<code>mongo [options] [db address] [file names]/<code>

由於上面運行的 mongodb 運行在 27017 端口,並且滅有啟動安全模式,所以我們也不需要輸入用戶名和密碼就可以直接連接:

<code>mongo 127.0.0.1:27017/<code>

或者通過 --host 和 --port 選項指定主機和端口。一切順利的話,就進入了 mongoDB shell,shell 會報出一連串權限警告,不過不用擔心,這並不會影響之後的操作。在添加授權用戶和開啟認證後,這些警告會自動消失。

MongoDB極簡教程

3.2 CRUD 操作

在進行增刪改查操作之前,我們需要先了解一下常用的 shell 命令:

  • db 顯示當前所在數據庫,默認為 test
  • show dbs 列出可用數據庫
  • show tables show collections 列出數據庫中可用集合
  • use 用於切換數據庫

mongoDB 預設有兩個數據庫,admin 和 local,admin 用來存放系統數據,local 用來存放該實例數據,在副本集中,一個實例的 local 數據庫對於其它實例是不可見的。使用 use 命令切換數據庫:

<code>> use admin
> use local
> use newDatabase/<code>

可以 use 一個不存在的數據庫,當你存入新數據時,mongoDB 會創建這個數據庫:

<code>> use newDatabase
> db.newCollection.insert({x:1})
WriteResult({ "nInserted" : 1 })/<code>

以上命令向數據庫中插入一個文檔,返回 1 表示插入成功,mongoDB 自動創建 newCollection 集合和數據庫 newDatabase。下面將對增查改刪操作進行一個簡單的演示。

3.2.1 創建(Create)

MongoDB 提供 insert 方法創建新文檔:

  • db.collection.inserOne() 插入單個文檔WriteResult({ "nInserted" : 1 })
  • db.collection.inserMany() 插入多個文檔
  • db.collection.insert() 插入單條或多條文檔

我們接著在剛才新創建的 newDatabase 下面新增數據吧:

<code>db.newCollection.insert({name:"wmyskxz",age:22})/<code>

根據以往經驗應該會覺得蠻奇怪的,因為之前在這個集合中插入的數據格式是 {x:1} 的,而這裡新增的數據格式確是 {name:"wmyskxz",age:22} 這個樣子的。還記得嗎,文檔型數據庫的與傳統型的關係型數據的區別就是在這裡!

並且要注意,age:22 和 age:"22" 是不一樣的哦,前者插入的是一個數值,而後者是字符串,我們可以通過 db.newCollection.find() 命令查看到剛剛插入的文檔:

<code>> db.newCollection.find()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }/<code>

這裡有一個神奇的返回,那就是多了一個叫做 _id 的東西,這是 MongoDB 為你自動添加的字段,你也可以自己生成。大部分情況下還是會讓 MongoDB 為我們生成,而且默認情況下,該字段是被加上了索引的。

3.2.2 查找(Read)

MongoDB 提供 find 方法查找文檔,第一個參數為查詢條件:

<code>> db.newCollection.find() # 查找所有文檔
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
> db.newCollection.find({name:"wmyskxz"}) # 查找 name 為 wmyskxz 的文檔
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
> db.newCollection.find({age:{$gt:20}}) # 查找 age 大於 20 的文檔
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }/<code>

上述代碼中的$gt對應於大於號>的轉義。

第二個參數可以傳入投影文檔映射數據:

<code>> db.newCollection.find({age:{$gt:20}},{name:1})
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz" }/<code>

上述命令將查找 age 大於 20 的文檔,返回 name 字段,排除其他字段。投影文檔中字段為 1 或其他真值表示包含,0 或假值表示排除,可以設置多個字段位為 1 或 0,但不能混合使用。

為了測試,我們為這個集合弄了一些奇奇怪怪的數據:

<code>> db.newCollection.find()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30 }/<code>

然後再來測試:

<code>> db.newCollection.find({age:{$gt:20}},{name:1,x:1}) 
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz" }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "x" : 1 }
> db.newCollection.find({age:{$gt:20}},{name:0,x:0}) 
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "age" : 22 }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "age" : 22, "y" : 30 }
> db.newCollection.find({age:{$gt:20}},{name:0,x:1})
Error: error: {
    "ok" : 0,
    "errmsg" : "Projection cannot have a mix of inclusion and exclusion.",
    "code" : 2,
    "codeName" : "BadValue"
}/<code>

從上面的命令我們就可以把我們的一些想法和上面的結論得以驗證,perfect!

除此之外,還可以通過 count、skip、limit 等指針(Cursor)方法,改變文檔查詢的執行方式:

<code>> db.newCollection.find().count()
3
> db.newCollection.find().skip(1).limit(10).sort({age:1})
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30 }/<code>

上述查找命令跳過 1 個文檔,限制輸出 10 個,以 age 子段正序排序(大於 0 為正序,小於 0 位反序)輸出結果。最後,可以使用 Cursor 方法中的 pretty 方法,提升查詢文檔的易讀性,特別是在查看嵌套的文檔和配置文件的時候:

<code>> db.newCollection.find().pretty()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{
    "_id" : ObjectId("5cc102fb33907ae66490e46d"),
    "name" : "wmyskxz",
    "age" : 22
}
{
    "_id" : ObjectId("5cc108fb33907ae66490e46e"),
    "name" : "wmyskxz-test",
    "age" : 22,
    "x" : 1,
    "y" : 30
}/<code>

3.2.3 更新(Update)

MongoDB 提供 update 方法更新文檔:

  • db.collection.updateOne() 更新最多一個符合條件的文檔
  • db.collection.updateMany() 更新所有符合條件的文檔
  • db.collection.replaceOne() 替代最多一個符合條件的文檔
  • db.collection.update() 默認更新一個文檔,可配置 multi 參數,跟新多個文檔

以 update() 方法為例。其格式:

<code>> db.collection.update(
    <query>,
    <update>,
    {
        upsert: <boolean>,
        multi: <boolean>
    }
)/<boolean>/<boolean>/<update>/<query>/<code>

各參數意義:

  • query 為查詢條件
  • update 為修改的文檔
  • upsert 為真,查詢為空時插入文檔
  • multi 為真,更新所有符合條件的文檔

下面我們測試把 name 字段為 wmyskxz 的文檔更新一下試試:

<code>> db.newCollection.update({name:"wmyskxz"},{name:"wmyskxz",age:30})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })/<code>

要注意的是,如果更新文檔只傳入 age 字段,那麼文檔會被更新為{age: 30},而不是{name:"wmyskxz", age:30}。要避免文檔被覆蓋,需要用到 $set 指令,$set 僅替換或添加指定字段:

<code>> db.newCollection.update({name:"wmyskxz"},{$set:{age:30}})/<code>

如果要在查詢的文檔不存在的時候插入文檔,要把 upsert 參數設置真值:

<code>> db.newCollection.update({name:"wmyskxz11"},{$set:{age:30}},{upsert:true})/<code>

update 方法默認情況只更新一個文檔,如果要更新符合條件的所有文檔,要把 multi 設為真值,並使用 $set 指令:

<code>> db.newCollection.update({age:{$gt:20}},{$set:{test:"A"}},{multi:true})
WriteResult({ "nMatched" : 3, "nUpserted" : 0, "nModified" : 3 })
> db.newCollection.find()

{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 30, "test" : "A" }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30, "test" : "A" }
{ "_id" : ObjectId("5cc110148d0a578f03d43e81"), "name" : "wmyskxz11", "age" : 30, "test" : "A" }/<code>

3.2.4 刪除(Delete)

MongoDB 提供了 delete 方法刪除文檔:

  • db.collection.deleteOne() 刪除最多一個符合條件的文檔
  • db.collection.deleteMany() 刪除所有符合條件的文檔
  • db.collection.remove() 刪除一個或多個文檔

以 remove 方法為例:

<code>> db.newCollection.remove({name:"wmyskxz11"})
> db.newCollection.remove({age:{$gt:20}},{justOne:true})
> db.newCollection.find()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30, "test" : "A" }/<code>

MongoDB 提供了 drop 方法刪除集合,返回 true 表面刪除集合成功:

<code>> db.newCollection.drop()/<code>

3.2.5 小結

相比傳統關係型數據庫,MongoDB 的 CURD 操作更像是編寫程序,更符合開發人員的直覺,不過 MongoDB 同樣也支持 SQL 語言。MongoDB 的 CURD 引擎配合索引技術、數據聚合技術和 JavaScript 引擎,賦予 MongoDB 用戶更強大的操縱數據的能力。

參考文章:簡明 MongoDB 入門教程 -

https://segmentfault.com/a/1190000010556670

4 MongoDB 數據模型的一些討論

前置申明:這一部分基於以下鏈接整理 https://github.com/justinyhuang/the-little-mongodb-book-cn/blob/master/mongodb.md#%E8%AE%B8%E5%8F%AF%E8%AF%81

這是一個抽象的話題,與大多數NoSQL方案相比,在建模方面,面向文檔的數據庫算是和關係數據庫相差最小的。這些差別是很小,但是並不是說不重要。

4.1 沒有連接(Join)

您要接受的第一個也是最基本的一個差別,就是 MongoDB 沒有連接(join)。我不知道MongoDB不支持某些類型連接句法的具體原因,但是我知道一般而言人們認為連接是不可擴展的。也就是說,一旦開始橫向分割數據,最終不可避免的就是在客戶端(應用程序服務器)使用連接。且不論MongoDB為什麼不支持連接,事實是數據是有關係的,可是MongoDB不支持連接。(譯者:這裡的關係指的是不同的數據之間是有關聯的,對於沒有關係的數據,就完全不需要連接。)

為了在沒有連接的MongoDB中生存下去,在沒有其他幫助的情況下,我們必須在自己的應用程序中實現連接。

基本上我們需要用第二次查詢去找到相關的數據。找到並組織這些數據相當於在關係數據庫中聲明一個外來的鍵。現在先別管什麼獨角獸了,我們來看看我們的員工。首先我們創建一個員工的數據(這次我告訴您具體的_id值,這樣我們的例子就是一樣的了):

<code>db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d730"), name: 'Leto'})/<code>

然後我們再加入幾個員工並把 Leto 設成他們的老闆:

<code>db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d731"), name: 'Duncan', manager: ObjectId("4d85c7039ab0fd70a117d730")});
db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d732"), name: 'Moneo', manager: ObjectId("4d85c7039ab0fd70a117d730")});/<code>

(有必要再強調一下,_id可以是任何的唯一的值。在實際工作中你很可能會用到ObjectId, 所以我們在這裡也使用它)

顯然,要找到Leto的所有員工,只要執行:

<code>db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")})/<code>

沒什麼了不起的。在最糟糕的情況下,為彌補連接的缺失需要做的只是再多查詢一次而已,該查詢很可能是經過索引了的。

4.1.1 數組和嵌入文檔(Embedded Documents)

MongoDB 沒有連接並不意味著它沒有其他的優勢。還記得我們曾說過 MongoDB 支持數組並把它當成文檔中的一級對象嗎?當處理多對一或是多對多關係的時候,這一特性就顯得非常好用了。用一個簡單的例子來說明,如果一個員工有兩個經理,我們可以把這個關係儲存在一個數組當中:

<code>({name: 'Siona', manager: [ObjectId("4d85c7039ab0fd70a117d730"), ObjectId("4d85c7039ab0fd70a117d732")] })/<code>

需要注意的是,在這種情況下,有些文檔中的 manager 可能是一個向量,而其他的卻是數組。在兩種情況下,前面的 find 還是一樣可以工作:

<code>db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")})/<code>

很快您就會發現數組中的值比起多對多的連接表(join-table)來說要更容易處理。

除了數組,MongoDB 還支持嵌入文檔。嘗試插入含有內嵌文檔的文檔,像這樣:

<code>db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d734"), name: 'Ghanima', family: {mother: 'Chani', father: 'Paul', brother: ObjectId("4d85c7039ab0fd70a117d730")}})/<code>

也許您會這樣想,確實也可以這樣做:嵌入文檔可以用‘.’符號來查詢:

<code>db.employees.find({'family.mother': 'Chani'})/<code>

就這樣,我們簡要地介紹了嵌入文檔適用的場合以及您應該怎樣使用它。

4.1.2 DBRef

MongoDB 支持一個叫做 DBRef 的功能,許多 MongoDB 的驅動都提供對這一功能的支持。當驅動遇到一個 DBRef 時它會把當中引用的文檔讀取出來。DBRef 包含了所引用的文檔的 ID 和所在的集合。它通常專門用於這樣的場合:相同集合中的文檔需要引用另外一個集合中的不同文檔。例如,文檔 1 的 DBRef 可能指向 managers 中的文檔,而文檔 2 中的 DBRef 可能指向 employees 中的文檔。

4.1.3 範規範化(Denormalization)

代替連接的另一種方法就是反規範化數據。在過去,反規範化是為性能敏感代碼所設,或者是需要數據快照(例如審計日誌)的時候才應用的。然而,隨著NoSQL的日漸普及,有許多這樣的數據庫並不提供連接操作,於是作為規範建模的一部分,反規範化就越來越常見了。這樣說並不是說您就需要為每個文檔中的每一條信息創建副本。與此相反,與其在設計的時候被複制數據的擔憂牽著走,還不如按照不同的信息應該歸屬於相應的文檔這一思路來對數據建模。

比如說,假設您在編寫一個論壇的應用程序。把一個 user 和一篇 post 關聯起來的傳統方法是在 posts 中加入一個 userid 的列。這樣的模型中,如果要顯示 posts 就不得不讀取(連接)users。一種簡單可行的替代方案就是直接把 name 和 userid 存儲在 post中。您甚至可以用嵌入文檔來實現,比如說 user: {id: ObjectId('Something'), name: 'Leto'}。當然,如果允許用戶更改他們的用戶名,那麼每當有用戶名修改的時候,您就需要去更新所有的文檔了(這需要一個額外的查詢)。

對一些人來說改用這種方法並非易事。甚至在一些情況下根本行不通。不過別不敢去嘗試這種方法:有時候它不僅可行,而且就是正確的方法。

4.1.4 應該選擇哪一種?

當處理一對多或是多對多問題的時候,採用id數組往往都是正確的策略。可以這麼說,DBRef並不是那麼常用,雖然您完全可以試著採用這項技術。這使得新手們在面臨選擇嵌入文檔還是手工引用(manual reference)時猶豫不決。

首先,要知道目前一個單獨的文檔的大小限制是 4MB,雖然已經比較大了。瞭解了這個限制可以為如何使用文檔提供一些思路。目前看來多數的開發者還是大量地依賴手工引用來維護數據的關係。嵌入文檔經常被使用,but mostly for small pieces of data which we want to always pull with the parent document。一個真實的例子,我把 accounts 文檔嵌入存儲在用戶的文檔中,就像這樣:

<code>db.users.insert({name: 'leto', email: '[email protected]', account: {allowed_gholas: 5, spice_ration: 10}})/<code>

這不是說您就應該低估嵌入文檔的作用,也不是說應該把它當成是鮮少用到的工具並直接忽略。將數據模型直接映射到目標對象上可以使問題變得更加簡單,也往往因此而不再需要連接操作。當您知道 MongoDB 允許對嵌入文檔的域進行查詢並做索引後,這個說法就尤其顯得正確了。

4.2 集合:少一些還是多一些?

既然集合不強制使用模式,那麼就完全有可能用一個單一的集合以及一個不匹配的文檔構建一個系統。以我所見過的情況,大部分的 MongoDB 系統都像您在關係數據庫中所見到的那樣佈局。換句話說,如果在關係數據庫中會用表,那麼很有可能在 MongoDB 中就要用集合(多對多連接表在這裡是一個不可忽視的例外)

當把嵌入文檔引進來的時候,討論就會變得更加有意思了。最常見的例子就是博客系統。是應該分別維護 posts 和 comments 兩個集合,還是在每個 post 中嵌入一個 comments 數組?暫且不考慮那個 4MB 的限制(哈姆雷特所有的評論也不超過200KB,誰的博客會比他更受歡迎?),大多數的開發者還是傾向於把數據劃分開。因為這樣既簡潔又明確。

沒有什麼硬性的規定(呃,除了 4MB 的限制)。做了不同的嘗試之後您就可以憑感覺知道怎樣做是對的了。

總結

至此已經對 MongoDB 有了一個基本的瞭解和入門,但是要運用在實際的項目中仍然有許多實踐需要自己去完成

- END -


分享到:


相關文章: