《MongoDB权威指南》迷你书连载一-入门篇

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 ”值。

 

posted @ 2011-06-24 09:35  我的IT技术  阅读(261)  评论(0编辑  收藏  举报