《MongoDB權威指南》迷你書連載一-入門篇

turingbooks發表於2020-04-07

MongoDB 非常強大,同時也很容易上手。本章會介紹一些 MongoDB 的基本概念。

  •   文件是 MongoDB 中資料的基本單元,非常類似與關係型資料庫中的行(但是比行要複雜得多)
  •   類似地,集可以被看作是沒有 schema 的表
  •  MongoDB 的單個例項可以容納多個獨立的資料庫,每一個都有自己的集和許可權。
  •  MongoDB 自帶簡潔但不簡單的 Javascript Shell ,這個工具對與管理 MongoDB 和運算元據作用非常大。
  •   每一個文件都有一個特殊的鍵“ _id ”,它在所處的集中是唯一的。

文件

文件是 MongoDB 的核心要義。多個鍵及其關聯的值有序的放置在一起便是文件 這種對文件的界定與程式語言不太一樣,但大多數程式語言都有種與之神似的資料結構,比如 map ,雜湊,字典。具體舉個例子,在 JavaScript 裡面,文件表示為物件:

{"greeting" : "Hello, world!"}

這個文件只有一個鍵“ greeting ”,其對應的值為“ Hello, world! ”。絕大多數情況下文件比這個簡單的例子會複雜得多,經常會包含多個鍵 / 值對兒:

{"greeting" : "Hello, world!", "foo" : 3}

這個例子很好地解釋了幾個十分重要的概念

+ 文件中的鍵 / 值對兒是有序的,上面的文件和下面的文件是完全不同的

{"foo" : 3, "greeting" : "Hello, world!"}

通常文件中的鍵的順序並不重要。實際上有些程式語言預設對文件的表達根本就不顧順序(如 Python 的字典, Perl Ruby 1.8 的中的雜湊)。這些語言的驅動會保有特殊的機制來應對小概率變為必然時那種要求有序的文件。

  • 文件中的值不光可以是在雙引號裡面的字串,還可以是以下資料型別(甚至可以是整個嵌入的文件 - 詳見“內嵌文件” XX 頁)。這個例子中“ greeting ”的值是個字串,而“ foo ”的值是個整數。

文件的鍵是字串。除了少數例外,鍵可以使用任意 UTF-8 字元:

  • 鍵不能含有 /0 (空字元)。這個字元用來表示鍵的結尾。
  • . $ 有特別的意義,只有在特定環境下才能使用,後面的章節會詳細說的。大體來說就是被保留了,使用不當的話,驅動程式會提示的。
  • 以下劃線“ _ ”開頭的的鍵是保留的,雖然這個並不是嚴格要求的。

MongoDB 不但型別敏感,大小寫也是敏感的。例如,下面的兩個文件是不同的:

{"foo" : 3}

{"foo" : "3"}

以下的文件也是不同的:

{"foo" : 3}

{"Foo" : 3}

還有一個非常重要的事項需要注意, MongoDB 總的文件不能有重複的鍵。例如,下面的文件是非法的

{"greeting" : "Hello, world!", "greeting" : "Hello, MongoDB!"}

集合

就是一組文件。要是文件之於 MongoDB 如同行對於關係型資料庫,那麼集就如同於表。

無模式

集是無模式的。這意味著一個集裡面的文件可以是各式各樣的。例如,下面兩個文件可以存在同一個集裡面:

{"greeting" : "Hello, world!"}

{"foo" : 5}

注意上面的文件不光是值的型別不同(字串 vs 整數),它們的鍵是完全不一樣的。因為集裡面可以放置任何文件,隨之而來的一個問題是:“那還有必要使用多個集麼?”非常好的問題,要是沒必要對各種文件劃分模式,那麼問什麼還要使用多個集呢?下面是一些理由:

把各種各樣的文件都混在一個集裡面,無論對於開發者還是管理員來說都是噩夢。開發者要麼確保每次查詢只返回需要的文件種類,要麼讓應用程式能處理所有不同型別的文件。如果查詢發表的部落格文章還要剔除那些含有作者資料的文件就很令人惱火。

  • 在一個集裡面查詢特定型別的文件在速度上也很不划算,分開做多個集要快得多。例如,集裡面有個標註型別的鍵,現在查詢它的值為“ skim “,” whole “,” chunky monkey “的文件,這會非常慢。如果按照名字分割成三個集的話,查詢會快很多(參看“子集“, XX 頁)
  • 把同種型別的文件放置在一起,這樣資料會更加集中。從只含有部落格文章的集裡面查詢幾篇文章會比從含有文章和使用者資料的集裡面獲得幾篇文章少消耗磁碟尋道操作。
  • 當建立索引的時候,文件會有附加的結構(尤其是唯一索引的時候)。索引是按照集來定義的。把同種型別的文件放入同一個集裡面,可以使索引更加有效。

你可能想到了,的確有很多理由建立一個結構把相關的文件規整到一起。當時 MongoDB 還是對此不做強制要求,給予開發者更大的靈活性。

Naming

命名

我們可以通過名字來區分集。集名除了下面這一點限制外,可以是任意的 UTF-8 的字串。

  • 集名不能是空字串“”
  •   集名不能含有 /0 字元(空字元),這個字元表示集名的結尾

集名不能以“ system. ”開頭,這是系統的保留字首。例如 system.user 這個集儲存著資料庫的使用者資訊, system.namespaces 集儲存著所有集的資訊。

使用者建立的集其名字不能含有保留字元 $ 。一些驅動的確支援集名裡面包含 $ 的,這是為了給那些系統建立的集用的。除非你要訪問這種系統建立的集,要不千萬不要在名字裡出現 $

子集

使用 "." 形成名稱空間,將很多集組成子集的形式非常方便。例如,一個帶有部落格功能的應用可能擁有兩個集分別是 blog.posts blog.authors 。這樣做的目的僅是組織結構更好些,也就是說 blog 這個集(這裡根本就不存在)及其子集沒有任何關係。

 

雖然子集沒有特別的地方,但還是很有用, [[ 很多 MongoDB 的工具都應用了這個。 ]]

l  GridFS 是一種儲存大檔案的協議,使用子集來儲存檔案的後設資料,這樣就與內容塊分開了(關於 GridFS 詳見第七章)

MongoDB WEB 控制檯通過子集的方式將資料組織在 DBTOP 部分(關於管理詳見第八章)。

絕大多數驅動都提供語法糖,為訪問指定集的子集提供方便。例如,在資料庫 shell 裡面, db.blog 代表 blog 集, db.blog.posts 代表 blog.posts 集。

MongoDB 中使用子集來組織資料是很好的實踐,在此強烈推薦。

資料庫

MongoDB 中多個文件組成集,同樣多個集可以組成資料庫。一個 MongoDB 例項可以執行多個資料庫,它們之間可視為完全獨立的。每個資料庫都有獨立的許可權控制,即便是在磁碟上不同的資料庫也是放置在不同的檔案中的。一個應用對應一個資料庫的做法就很好。要想在同一個 MongoDB 伺服器上存放多個應用或者使用者的資料,就要使用不同的資料庫了

和集一樣,資料庫也通過名字來區分。資料庫名可以是滿足一下條件的任意 UTF-8 的字串。

  • 不能是空字串 (“”)
  • 不得含有‘ ’(空格), . $ / / /0 (空字元)
  • 應全部小寫
  • 最多 64 位元組

要記住一點,資料庫名最終會變成檔案系統裡的檔案,這也就是為什麼有如此多的限制。

有一些資料庫名是保留的,可以直接訪問這些有特殊作用的資料庫。具體是:

admin

從許可權的角度來看,這是“ root ”資料庫。要是將一個使用者新增到這個資料庫,這個使用者自動繼承所有資料庫的許可權。一些特定的伺服器端的命令也只能從這個資料庫執行,比如列出所有的資料庫或者關閉伺服器。

local

這個資料永不會被複制,可以用來儲存限於單臺伺服器的配置集(關於複製和本地資料庫詳見第九章)

Mongo 以分片(見第十章), config 資料庫儲存著分片的相關資訊。

把資料庫的名字放到集名前面,得到就是集的全名,稱為名稱空間 . 例如,如果你在 cms 資料庫使用 blog.posts 集,那麼這個集的名稱空間則為 cms.blog.posts 。名稱空間長度不得超過 121 位元組,在實際使用當中應該小於 100 位元組。關於 MongoDB 中集的名稱空間和內部表達的更多資訊可以參考附錄 C

啟動 MongoDB

MongoDB 幾乎總是作為網路服器務來執行的,客戶端可以連線並執行操作。要啟動該伺服器,需要執行 mongod

$ ./mongod

./mongod --help for help and startup options

Sun Mar 28 12:31:20 Mongo DB : starting : pid = 44978 port = 27017

dbpath = /data/db/ master = 0 slave = 0 64-bit

Sun Mar 28 12:31:20 db version v1.5.0-pre-, pdfile version 4.5

Sun Mar 28 12:31:20 git version: ...

Sun Mar 28 12:31:20 sys info: ...

Sun Mar 28 12:31:20 waiting for connections on port 27017

Sun Mar 28 12:31:20 web admin interface listening on port 28017

或者在 Windows 下,這樣操作:

$ mongod.exe

關於安裝 MongoDB 的詳細資訊,參看附錄 A

mongod 在沒有引數情況下會使用預設資料目錄 /data/db ( Windows 下是 C:/data/db/), 並使用 27017 埠。如果資料目錄不存在或者不可寫,伺服器會啟動失敗。所以在啟動 MongoDB 前,很重要的就是要建立資料目錄(比如 mkdir -p /data/db ),並確保對該目錄有可寫許可權。如果埠被佔用,啟動也會失敗的。通常這是由於 MongoDB 例項已經在執行了。伺服器會列印版本和系統資訊,然後等待連結。預設情況下, MongoDB 監聽 27017 埠。 mongod 還會啟動一個非常基本 HTTP 伺服器,監聽數字比上一個多 1000 的埠,這裡也就是 28017 埠。這意味著你可以通過瀏覽器訪問 http://localhost:28017 來獲取資料庫的管理資訊。

在啟動伺服器的 shell 下可以鍵入 Ctrl-c 來終止 mongod

想要了解啟動和停止 MongoDB 的更多細節,請參看 XXX 頁“啟動和停止 MongoDB ”,想要了解管理介面的更多內容,可以參考 XXX 頁的“使用管理介面”

MongoDB Shell

MongoDB 自帶一個 Javascript shell 可以從命令列與 MongoDB 例項互動。這個 shell 非常有用處,管理操作,監控執行例項,亦或是乾脆玩玩都要仰賴它。這個 shell 是使用 MongoDB 和核心工具,本書後面也會貫穿使用這個工具的。

執行 shell

執行 mongo 啟動 shell

$ ./mongo

MongoDB shell version: 1.6.0

url: test

connecting to: test

type "help" for help

shell 會在啟動時自動連線 MongoDB 伺服器,所以要確保在使用 shell 之前開啟 mongod

shell 是全功能的 JavaScript 直譯器,可以執行任何 JavaScript 程式。為了證明,讓我們執行幾個簡單的運算

> x = 200

200

> x / 5;

40

還可以充分利用 JavaScript 的標準庫。

> Math.sin(Math.PI / 2);

1

> new Date("2010/1/1");

"Fri Jan 01 2010 00:00:00 GMT-0500 (EST)"

> "Hello, World!".replace("World", "MongoDB");

Hello, MongoDB!

也可以定義呼叫 JavaScript 函式:

> function factorial (n) {

... if (n <= 1) return 1;

... return n * factorial(n - 1);

... }

> factorial(5);

120

注意:可以使用多行命令。這個 shell 會檢測輸入的 JavaScript 語句是否完整,如不完整你可以在下一行接著寫。

MongoDB 客戶端

雖然能執行任意 JavaScript 程式很舒服,但 shell 的真正威力還在於它是一個獨立的 MongoDB 客戶端。開啟的時候, shell 會連到 MongoDB 伺服器的 test 資料庫,並將這個資料庫連結賦值給全域性變數 db 。這個變數通過 shell 訪問 MongoDB 的入口點。

shell 還有些外掛,本身不符合 JavaScript 語法,為了方便習慣於 SQL 的使用者而新增的。這些外掛並不提供額外的功能,僅僅是些語法糖。例如,最重要的操作之一就是選擇要使用的資料庫:

> use foobar

switched to db foobar

Now if you look at the db variable, you can see that it refers to the foobar database:

現在如果看看 db ,會發現其指向 foobar 資料庫

> db

foobar

因為這是一個 JavaScript shell ,所以鍵入一個變數會將變數的值轉換為字串(這裡就是資料庫名)並列印出來。

可以通過 db 這個變數來訪問其中的集。例如 db.baz 返回當前資料庫的 baz 集。既然現在可以在 shell 中訪問集,那麼基本上可以執行幾乎所有資料庫操作了。

shell 中的基本操作

shell 檢視運算元據會用到四個基本操作, 建立 create, 讀取 read, 更新 update, 刪除 delete (CRUD)

建立

insert 函式新增一個文件到集裡面。例如,假設要儲存一篇部落格文章。首先,建立一個區域性變數 post ,內容是代表文章的文件的 JavaScript 物件。裡面會有“ title ”,“ content ”,“ date ”(發表日期)幾個鍵。

> post = {"title" : "My Blog Post",

... "content" : "Here's my blog post.",

... "date" : new Date()}

{

"title" : "My Blog Post",

"content" : "Here's my blog post.",

"date" : "Sat Dec 12 2009 11:23:21 GMT-0500 (EST)"

}

這個物件是個有效的 MongoDB 文件,所以可以用 insert 方法將其儲存到 blog 集:

> db.blog.insert(post)

這篇文章已經被存到資料庫裡面了。可以在集上用 find 來檢視一下。

> db.blog.find()

{

"_id" : ObjectId("4b23c3ca7525f35f94b60a2d"),

"title" : "My Blog Post",

"content" : "Here's my blog post.",

"date" : "Sat Dec 12 2009 11:23:21 GMT-0500 (EST)"

}

除了我們輸入的鍵 / 值對兒都完整被儲存下來,還有一個額外新增的鍵“ _id ”。本章的最後會解釋“ _id ”的突然出現。

讀取

find 會返回集裡面所有的文件。若只是想看一個文件,可以用 findOne

> db.blog.findOne()

{

"_id" : ObjectId("4b23c3ca7525f35f94b60a2d"),

"title" : "My Blog Post",

"content" : "Here's my blog post.",

"date" : "Sat Dec 12 2009 11:23:21 GMT-0500 (EST)"

}

find findOne 可以接受查詢文件形式的限定條件。 [[]]shell 自動顯示不超過 20 個匹配的文件,其他的也可以獲得。關於查詢的更多內容,參看第四章。

更新

如果更改了部落格文章,就要用到 update 了。 update 接受(至少)兩個引數:第一個是要更新文件的限定條件,第二個是新的文件。假設決定給我先前寫得文章增加評論功能。

則需要增加一個新的鍵,對應的值是存放評論的陣列。

第一步修改變數增加“ comments ”鍵:

> post.comments = []

[ ]

Then we perform the update, replacing the post titled “My Blog Post” with our new

version of the document:

然後執行 update 操作,用新的版本替換標題為“ My Blog Post ”的文章:

> db.blog.update({title : "My Blog Post"}, post)

文件已經有了“ comments ”鍵。再用 find 檢視一下,可以看到新的鍵:

> db.blog.find()

{

"_id" : ObjectId("4b23c3ca7525f35f94b60a2d"),

"title" : "My Blog Post",

"content" : "Here's my blog post.",

"date" : "Sat Dec 12 2009 11:23:21 GMT-0500 (EST)"

"comments" : [ ]

}

刪除

remove 用來從資料庫中永久性地刪除文件。在沒有引數呼叫的情況下,它會刪除一個集內的所有文件。

它也可以接受一個用以指定限定條件的文件。例如,下面的命令會刪除我們剛剛建立的文章:

> db.blog.remove({title : "My Blog Post"})

集現在又是空的了。

使用 shell 的竅門

由於 mongo 是個 JavaScript shell ,通過線上檢視 JavaScript 的文件能獲得很多幫助。 shell 本身內建了幫助文件,可以通過 help 命令檢視。

> help

HELP

show dbs

show collections

show users

show profile

use <db name>

db.help()

db.foo.help()

db.foo.find()

db.foo.find( { a : 1 } )

it

show database names

show collections in current database

show users in current database

show recent system.profile entries w. time >= 1ms

set current database to <db name>

help on DB methods

help on collection methods

list objects in collection foo

list objects in foo where a == 1

result of the last line evaluated

db.help() 可以檢視資料庫級別的命令的幫助,同樣集的相關幫助可以通過 db.foo.help() 來檢視。

有個瞭解函式功用的技巧就是輸入的時候不要輸括號。這樣就會顯示該函式的 JavaScript 原始碼。例如,如果想看看 update 的機理,或者就是為了看看引數順序,可以這麼做:

> db.foo.update

function (query, obj, upsert, multi) {

assert(query, "need a query");

assert(obj, "need an object");

this._validateObject(obj);

this._mongo.update(this._fullName, query, obj,

}

upsert ? true : false, multi ? true : false);

shell 提供的 API 文件,可以參看網址 http://api.mongodb.org/js.

不方便的集名

使用 "db. 集名 " 的方式來訪問集一般不會有問題,但是要是集名恰好是資料庫類的一個屬性就有問題了。例如,要訪問 version 這個集,使用 db.version 就不行,因為 db.version 是個資料庫函式(這個函式返回正在執行的 MongoDB 伺服器的版本)。

> db.version

function () {

return this.serverBuildInfo().version;

}

JavaScript 只有在 db 中找不到指定的屬性時候,才會將其作為集返回。當有屬性與目標集同名時,可以採用 getCollection 函式:

> db.getCollection("version");

test.version

這對於使用在 JavaScript 中不合法名字作為集名也很有用。比如 foo-bar 是個有效的集名,但是 JavaScript 中就變成了變數相減了。

JavaScript 中, x.y x['y'] 完全等價。這就意味著不但可以直呼其名,也可以使用變數來訪問子集。進一步說,當需要對 blog 的每個子集操作時,只需要向下面這樣迭代就好了:

var collections = ["posts", "comments", "authors"];

for (i in collections) {

doStuff(db.blog[collections[i]]);

}

比較一下笨笨的寫法:

doStuff(db.blog.posts);

doStuff(db.blog.comments);

doStuff(db.blog.authors);

資料型別

本章的開始講了些文件的基本概念。現在大家可以執行個 MongoDB ,在 shell 裡面動手試試。這一部分會更加深入一些。 MongoDB 的文件資料型別非常豐富。本節我們就來逐一看看。

基本資料型別

MongoDB 的文件是“類 JSON “樣式的,和 JavaScript 中的物件神似。 JSON 是種簡單表示資料方式,具體規格說明就一段文字(請到 httpXXX 自行驗證),僅包含六種資料型別。著帶來很多好處:易於理解,易於解析,易於記憶。但另外一方面, JSON 的表現力也有瓶頸,因為只有 null ,布林,數字,字串,陣列和物件幾種型別。

雖然這些型別的表現力已經足夠強大,但是對於絕大多數應用來說還需要另外一些不可或缺的型別,尤其是與資料庫打交道的那些應用。例如, JSON 沒有日期型別,這會使得處理本來簡單的日期問題變得非常繁瑣。只有一種數字型別,沒法區分浮點數和整數,也不能區分 32 位和 64 位數。也沒有辦法表達其他常用型別,如正則和函式。

MongoDB 在保持 JSON 基本的鍵 / 值對錶達方式的基礎上,新增了一些資料型別。在不同的程式語言下這些型別的實現有些許差異,下面是一個全面適用的型別列表,在 shell 中這些型別可以在文件中作用也有說明。

null

Null 用在空值,或者不存在的區域

{"x" : null}

布林

布林型別有兩個值‘ true ‘和‘ false

{"x" : true}

32 位整數

shell 中這個型別不可用。前面提到, JavaScript 僅支援 64 位浮點數,所以 32 位整數自動被轉換了。

64 位整數

這個 shell 也不支援。 shell 將會將其顯示成一個特殊的內嵌文件;詳見 XX 頁“數字”一節

64 位浮點數

shell 中的數都是這種型別。所以下面的是一個浮點數

{"x" : 3.14}

As will this:

這個也是浮點數:

{"x" : 3}

string

字串

UTF-8 字串都可作為字串型別的資料:

{"x" : "foobar"}

符號

shell 不支援這種型別。 shell 將資料庫裡的符號型別被轉換成字串。

物件 id

物件 id 是文件的 12 位元組的唯一 ID 。詳見 xx 頁“ _id ObjectId ”一節

{"x" : ObjectId()}

日期

日期型別儲存的是從標準紀元開始的微妙數。不記錄時區:

{"x" : new Date()}

正規表示式

文件中可以包含正規表示式,採用 JavaScript 語法:

{"x" : /foobar/i}

程式碼

文件中還可包含 JavaScript 程式碼

{"x" : function() { /* ... */ }}

二進位制資料

二進位制資料可以由任意位元組的串組成。 shell 下不可對其操作。

最大值

BSON 包括一個特殊型別,來表示可能的最大值。 shell 中沒有這個型別。

最小值

BSON 包括一個特殊型別,來表示可能的最小值。 shell 中沒有這個型別。

未定義

文件中也可以使用未定義型別( JavaScript null 和未定義是不一樣的)

{"x" : undefined}

array

陣列

Sets or lists of values can be represented as arrays:

值的集合或者列表可以表示成陣列: [[ 可以使用陣列來表達一組值 ]]

{"x" : ["a", "b", "c"]}

embedded document

內嵌文件

Documents can contain entire documents, embedded as values in a parent

document:

文件可以包含別的文件,也可以作為一個值嵌入到其他文件中:

{"x" : {"foo" : "bar"}}

數字

JavaScript 中只有一種“數字”型別。因為 MongoDB 中需要有三種數字型別( 32 位整數, 64 位整數, 64 位浮點數), shell JavaScript 做了些修補。預設情況下, shell 中的數字都被 MongoDB 當作是 64 位浮點數。

這意味著要是本來文件中是個 32 位整數,修改文件後,將文件回存的時候,這個整數也被轉換成了浮點數,即便保持這個數原封不動也會這樣的。所以明智的做法是儘量不要在 shell 下覆蓋這個文件。(關於修改指定鍵的值參看第三章)

數字只有雙精度( 64 位浮點數)另外一個問題是有些 64 位的整數並不能精確地轉換為 64 位浮點數。所以,要是存入了一個 64 位整數,然後在 shell 中檢視,它會顯示一個內嵌文件,並提示可能不準確。例如,儲存一個文件(譯者注,顯然不是在 shell 中儲存的,要不作者就白說了),其中“ myInteger ”鍵的值設為一個 64 位整數—— 3 ,然後在 shell 中觀察一下,應該是這樣的:

> doc = db.nums.findOne()

{

"_id" : ObjectId("4c0beecfd096a2580fe6fa08"),

"myInteger" : {

"floatApprox" : 3

}

}

在資料庫中的數字是不會改變的(除非你修改了,爾後又在 shell 裡儲存回去了,這樣就會被轉換成浮點型別);內嵌文件只是提示 shell 顯示的是一個用 64 浮點數近似表示的 64 位整數。若是內嵌文件只有一個鍵的話,實際上這個值是準確的。

要是插入的 64 位整數不能精確地作為雙精度數顯示, shell 會新增兩個鍵,“ top ”和“ bottom ”,分別表示高 32 位和低 32 位。例如,如果插入 9223372036854775807 shell 會這樣顯示:

> db.nums.findOne()

{

"_id" : ObjectId("4c0beecfd096a2580fe6fa09"),

"myInteger" : {

"floatApprox" : 9223372036854776000,

"top" : 2147483647,

"bottom" : 4294967295

}

}

The "floatApprox" embedded documents are special and can be manipulated as numbers as well as documents:

floatApprox ”是種特殊的內嵌文件, 可以像操作其他文件的數一樣來來操作

> doc.myInteger + 1

4

> doc.myInteger.floatApprox

3

32 位的整數都能用 64 位的浮點數精確表示,所以顯示起來沒什麼特別的。

日期

不使用 new 的方式),實際上會返回對日期的字串表示,而不是真正的 Date 物件。這不是 MongoDB 的特性,這是 JavaScript 本身的特性。要是不小心使用 Date 構造器,最後就會導致日期和字串混作一團。字串和日期不能互相匹配,所以這會給刪除,更新,查詢,差不多所有操作帶來問題。

關於 JavaScript Date 類的詳細說明和構造器適用形式,請參看 ECMAScript 規格文件 15.9 節(可在 http://www.ecmascript.org 下載)

shell 中的日期顯示時使用本地時區設定。但是,日期在資料中儲存的就是從標準紀元開始的微秒數,是沒有時區資訊的。(當然可以把時區資訊存在其他鍵 / 值中)

陣列

陣列是一組值,既可以作為有序物件來(可以想象成列表,堆疊,佇列)操作,也可以作為無序物件操作(想象成集合)。

在下面的文件中,“ things ”這個鍵的值就是一個陣列:

{"things" : ["pie", 3.14]}

從例子可以看到,陣列可以包含不同資料型別的元素(這個例子中,一個字串和一個浮點數)。實際上,可以作為鍵的值都可以作為陣列的元素,甚至是內嵌陣列。

文件中陣列有個奇妙的特性, MongoDB “理解”其結構,並知道如何“深入”陣列內部對其內容進行操作。這樣就能用內容對陣列查詢和構建索引了。

例如,之前的例子中, MongoDB 可以查詢所有“ things ”中含有 3.14 的文件。要是經常使用這個查詢,可以對“ things ”做索引,來提高效能。

MongoDB 可以使用原子更新修改陣列中的內容,比如深入陣列內部將 "pie" 改為 "pi" 。在本書中還會更多這種操作的例子。

內嵌文件

內嵌文件就是把整個 MongoDB 文件作為一個值插入到另外一個文件中。這樣資料可以組織的更自然些,不用非得存成扁平結構的。

例如,用一個文件來表示一個人,同時還要儲存他的地址,可以將地址內嵌到文件中:

{

}

"name" : "John Doe",

"address" : {

"street" : "123 Park Street",

"city" : "Anytown",

"state" : "NY"

}

上個例子中“ address ”的值是又一個的文件,這個文件有自己的“ street ”,“ city ”和“ state ”鍵值。

同陣列一樣, MongoDB 能夠“理解”內嵌文件的結構,並能“深入”其中構建索引,執行查詢,或者更新。

我們會在後面深入討論模式設計,但就算是從這個簡單的例子也可以看出內嵌文件可以改變處理資料的方式。在關係型資料庫中,之前的文件一般會被拆解成兩個表(“ people ”和“ address ”)中的兩行。在 MongoDB 中,就可以將地址文件直接嵌入人員文件。使用得當的話,內嵌文件會使資訊表達更加自然(通常也會更高效)。

這樣做也有壞處, MongoDB 儲存了更多重複的資料,這樣是反正規化化的。如果在關聯式資料庫中“ address ”在一個獨立的表中,要修復地址中的拼寫錯誤。當我們對“ people ”和“ address [[join]] 操作時,每一個使用這個地址的人的資訊都會得到更新。但是在 MongoDB 中,需要對每個人資訊逐個修改。

_id ObjectId

MongoDB 中儲存的文件必須有一個“ _id ”鍵。這個鍵的值可以是任何型別的,預設是個 ObjectId 物件。在一個集裡面,每個文件都有唯一的“ _id ”值,來確保集裡面每個文件都能被唯一定位。如果有兩個集的話,兩個個集可以都有一個值為 123 的“ _id ”鍵。但是一個集裡面只能有一個“ _id ”是 123 的文件。

ObjectId

ObjectId 是“ _id ”的預設型別。其設計的思路就是輕量的,不同地方的機器都能用同種方法方便地生成。 MongoDB 採用 ObjectId ,而不是比較常規的做法,比如自增加的主鍵,是有其原因的:在多個機器上同步自增長的主鍵既費力還費時。 MogoDB 從開始就設計用來做分散式資料庫,處理多個節點是一個核心要素。後面會看到 ObjectId 型別在分片環境中要容易生成得多。

實際上使用的儲存空間只有那個長串的一半。

如果快速連續生成多個 ObjectId ,會發現只有最後幾位有變化。另外,中間的幾位也會變化(要是中間停頓幾秒鐘)。這是 ObjectId 生成規則導致的。 12 個位元組按照如下方式產生:

前四個位元組是標準紀元的秒數。這會帶來一些性質:

  • 時間戳,與隨後的五個位元組組合起來,提供了秒級別的唯一性。
  • 由於時間戳在前,這意味著 ObjectId 大致會按照插入的順序排列。這對於某些方面很有用,如將其作為索引提高效率,但是這個是沒有保證的,僅僅是“大致”。
  • 這四個位元組也隱含了文件建立的時間。絕大多數驅動都會提供一個方法來解析這個資訊的。

因為使用的是當前時間,很多使用者擔心要對伺服器進行時間同步。其實沒有這個必要,因為時間的實際值並不重要,只要其總是不停增長就好了(每秒一次)。接下來的三個位元組是所在主機的唯一辨識符。通常是機器主機名的雜湊值。這樣就可以確保不同主機生成不同的 ObjectId ,不產生衝撞。

為了確保在同一臺機器上併發的多個程式產生的 ObjectId 是唯一的,接下來的兩個位元組就是產生 ObjectId 的程式號( PID )。

前九個位元組保證了同一秒鐘不同機器不同程式產生的 ObjectId 是唯一的。後三個位元組就是一個自增加計數器,確保同個程式同一秒產生的 ObjectId 也是不一樣的。

自動生成 _id

前面講到,如果插入文件的時候沒有“ _id ”鍵,系統會自動幫你建立一個。可以由伺服器來做這個事情,將來會在客戶端由驅動程式完成。理由如下:

雖然 ObjectId 設計上就是輕量,易於生成的,但是畢竟生成的時候還是產生開銷。在客戶端生成體現了 MongoDB 的設計理念:能從服務端轉移到驅動程式來做的事兒,就儘量轉移。這種理念背後的原因是,即便是像 MongoDB 這樣的可擴充套件資料庫,擴充套件應用層也要比擴充套件資料庫層容易得多。將事務交由客戶端來做,減輕了資料庫擴充套件的負擔。

  • 在客戶端生成 ObjectId ,驅動程式能夠提供更加豐富的 API 。例如,驅動程式可以自己的插入方法,可以返回生成的 ObjectId ,也可以直接將其插入文件。如果驅動程式允許伺服器生成 ObjectId, 那麼將需要單獨的查詢,以確定插入的文件中的“ _id ”值。

 

相關文章