第2章 MongoDB基础知识
MongoDB非常强大但很容易上手。本章会介绍一些MongoDB的基本概念。
- 文档是MongoDB中数据的基本单元,非常类似于关系型数据库管理系统中的行,但更具表现力。
- 类似地,集合(collection)可以看作是一个拥有动态模式(dynamic schema)的表。
- MongoDB的一个实例可以拥有多个相互独立的数据库(database),每一个数据库都拥有自己的集合。
- 每一个文档都有一个特殊的键
"_id"
, 这个键在文档所属的集合中是唯一的。 - MongoDB自带了一个简单但功能强大的JavaScript shell,可用于管理MongoDB的实例或数据操作。
2.1 文档
文档是MongoDB的核心概念。文档就是键值对的一个有序集。每种编程语言表示文档的方法不太一样,但大多数编程语言都有一些相通的数据结构,比如映射(map)、散列(hash)或字典(dictionary)。例如,在JavaScript 里面,文档被表示为对象:
{"greeting" : "Hello, world!"}
这个文档只有一个键"greeting"
,其对应的值为"Hello,world!"
。大多数文档会比这个简单的例子复杂得多,通常会包含多个键/值对:
{"greeting" : "Hello, world!", "foo" : 3}
从上面的例子可以看出,文档中的值可以是多种不同的数据类型(甚至可以是一个完整的内嵌文档,详见2.6.4节)。在这个例子中,"greeting"
的值是一个字符串,而"foo"
的值是一个整数。
文档的键是字符串。除了少数例外情况,键可以使用任意UTF-8字符。
- 键不能含有\0(空字符)。这个字符用于表示键的结尾。
- .和$具有特殊意义,只能在特定环境下使用(后面的章节会详细说明)。通常,这两个字符是被保留的;如果使用不当的话,驱动程序会有提示。
MongoDB不但区分类型,而且区分大小写。例如,下面的两个文档是不同的:
{"foo" : 3}
{"foo" : "3"}
下面两个文档也是不同的:
{"foo" : 3}
{"Foo" : 3}
还有一个非常重要的事项需要注意,MongoDB的文档不能有重复的键。例如,下面的文档是非法的:
{"greeting" : "Hello, world!", "greeting" : "Hello, MongoDB!"}
文档中的键/值对是有序的:{"x" : 1, "y":2}
与{"y": 2, "x": 1}
是不同的。通常,字段顺序并不重要,无须让数据库模式依赖特定的字段顺序(MongoDB会对字段重新排序)。在某些特殊情况下,字段顺序变得非常重要,本书将就此给出提示。
一些编程语言对文档的默认表示根本就不包含顺序问题(如:Python中的字典、Perl和Ruby 1.8中的散列)。通常,这些语言的驱动具有某些特殊的机制,可以在必要时指定文档的顺序。
2.2 集合
集合就是一组文档。如果将MongoDB中的一个文档比喻为关系型数据库中的一行,那么一个集合就相当于一张表。
2.2.1 动态模式
集合是动态模式的。这意味着一个集合里面的文档可以是各式各样的。例如,下面两个文档可以存储在同一个集合里面:
{"greeting" : "Hello, world!"}
{"foo" : 5}
需要注意的是,上面的文档不光值的类型不同(一个是字符串,一个是整数),它们的键也完全不同。因为集合里面可以放置任何文档,随之而来的一个问题是:还有必要使用多个集合吗?这的确值得思考:既然没有必要区分不同类型文档的模式,为什么还要使用多个集合呢?这里有几个重要的原因。
如果把各种各样的文档不加区分地放在同一个集合里,无论对开发者还是对管理员来说都将是噩梦。开发者要么确保每次查询只返回特定类型的文档,要么让执行查询的应用程序来处理所有不同类型的文档。如果查询博客文章时还要剔除含有作者数据的文档,这会带来很大困扰。
在一个集合里查询特定类型的文档在速度上也很不划算,分开查询多个集合要快得多。例如,假设集合里面一个名为
"type"
的字段用于指明文档是skim、whole还是chunky monkey。那么,如果从一个集合中查询这三种类型的文档,速度会很慢。但如果将这三种不同类型的文档拆分为三个不同的集合,每次只需要查询相应的集合,速度快得多。把同种类型的文档放在一个集合里,数据会更加集中。从一个只包含博客文章的集合里查询几篇文章,或者从同时包含文章数据和作者数据的集合里查出几篇文章,相比之下,前者需要的磁盘寻道操作更少。
创建索引时,需要使用文档的附加结构(特别是创建唯一索引时)。索引是按照集合来定义的。在一个集合中只放入一种类型的文档,可以更有效地对集合进行索引。
上面这些重要原因促使我们创建一个模式,把相关类型的文档组织在一起,尽管MongoDB对此并没有强制要求。
2.2.2 命名
集合使用名称进行标识。集合名可以是满足下列条件的任意UTF-8字符串。
- 集合名不能是空字符串("")。
- 集合名不能包含\0字符(空字符),这个字符表示集合名的结束。
- 集合名不能以“system.”开头,这是为系统集合保留的前缀。例如,system.users这个集 合保存着数据库的用户信息,而system.namespaces集合保存着所有数据库集合的信息。
- 用户创建的集合不能在集合名中包含保留字符'$'。因为某些系统生成的集合中包含
$
,很多驱动程序确实支持在集合名里包含该字符。除非你要访问这种系统创建的集合,否则不应该在集合名中包含$
。
子集合
组织集合的一种惯例是使用“.”分隔不同命名空间的子集合。例如,一个具有博客功能的应用可能包含两个集合,分别是blog.posts和blog.authors。这是为了使组织结构更清晰,这里的blog集合(这个集合甚至不需要存在)跟它的子集合没有任何关系。
虽然子集合没有任何特别的属性,但它们却非常有用,因而很多MongoDB工具都使用了子集合。
- GridFS(一种用于存储大文件的协议)使用子集合来存储文件的元数据,这样就可以与文件内容块很好地隔离开来。(第6章会详细介绍GridFS。)
- 大多数驱动程序都提供了一些语法糖,用于访问指定集合的子集合。例如,在数据库shell中,
db.blog
代表blog集合,而db.blog.posts
代表blog.posts集合。
在MongoDB中,使用子集合来组织数据非常高效,值得推荐。
2.3 数据库
在MongoDB中,多个文档组成集合,而多个集合可以组成数据库。一个MongoDB实例可以承载多个数据库,每个数据库拥有0个或者多个集合。每个数据库都有独立的权限,即便是在磁盘上,不同的数据库也放置在不同的文件中。按照经验,我们将有关一个应用程序的所有数据都存储在同一个数据库中。要想在同一个MongoDB服务器上存放多个应用程序或者用户的数据,就需要使用不同的数据库。
数据库通过名称来标识,这点与集合类似。数据库名可以是满足以下条件的任意UTF-8字符串。
- 不能是空字符串("")。
- 不得含有/、\、.、"、*、<、>、:、|、?、$(一个空格)、\0(空字符)。基本上,只能使用ASCII中的字母和数字。
- 数据库名区分大小写,即便是在不区分大小写的文件系统中也是如此。简单起见,数据库名应全部小写。
- 数据库名最多为64字节。
要记住一点,数据库最终会变成文件系统里的文件,而数据库名就是相应的文件名,这是数据库名有如此多限制的原因。
另外,有一些数据库名是保留的,可以直接访问这些有特殊语义的数据库。这些数据库如下所示。
- admin
从身份验证的角度来讲,这是“root”数据库。如果将一个用户添加到admin数据库,这个用户将自动获得所有数据库的权限。再者,一些特定的服务器端命令也只能从admin数据库运行,如列出所有数据库或关闭服务器。
- local
这个数据库永远都不可以复制,且一台服务器上的所有本地集合都可以存储在这个数据库中。(第9章会详细介绍复制及本地数据库。)
- config
MongoDB用于分片设置时(参见第13章),分片信息会存储在config数据库中。
把数据库名添加到集合名前,得到集合的完全限定名,即命名空间(namespace)。例如,如果要使用cms数据库中的blog.posts集合,这个集合的命名空间就是cms.blog.posts
。命名空间的长度不得超过121字节,且在实际使用中应小于100字节。(参考附录B,了解MongoDB中集合的命名空间及内部表示的更多信息。)
2.4 启动MongoDB
通常,MongoDB作为网络服务器来运行,客户端可连接到该服务器并执行操作。下载MongoDB(http://www.mongodb.org/downloads)并解压,运行mongod
命令,启动数据库服务器:
$ mongod
mongod --help for help and startup options
Thu Oct 11 12:36:48 [initandlisten] MongoDB starting : pid=2425 port=27017
dbpath=/data/db/ 64-bit host=spock
Thu Oct 11 12:36:48 [initandlisten] db version v2.4.0, pdfile version 4.5
Thu Oct 11 12:36:48 [initandlisten] git version:
3aaea5262d761e0bb6bfef5351cfbfca7af06ec2
Thu Oct 11 12:36:48 [initandlisten] build info: Darwin spock 11.2.0 Darwin Kernel
Version 11.2.0: Tue Aug 9 20:54:00 PDT 2011;
root:xnu-1699.24.8~1/RELEASE_X86_64 x86_64 BOOST_LIB_VERSION=1_48
Thu Oct 11 12:36:48 [initandlisten] options: {}
Thu Oct 11 12:36:48 [initandlisten] journal dir=/data/db/journal
Thu Oct 11 12:36:48 [initandlisten] recover : no journal files present, no
recovery needed
Thu Oct 11 12:36:48 [websvr] admin web console waiting for connections on
port 28017
Thu Oct 11 12:36:48 [initandlisten] waiting for connections on port 27017
在Windows系统中,执行这个命令:
$ mongod.exe
关于安装MongoDB的详细信息,参见附录A。
mongod
在没有参数的情况下会使用默认数据目录/data/db(Windows系统中为C:\data\db)。如果数据目录不存在或者不可写,服务器会启动失败。因此,在启动MongoDB前,先创建数据目录(如mkdir -p /data/db/),以确保对该目录有写权限,这点非常重要。
启动时,服务器会打印版本和系统信息,然后等待连接。默认情况下,MongoDB监听27017端口。如果端口被占用,启动将失败。通常,这是由于已经有一个MongoDB实例在运行了。
mongod
还会启动一个非常基本的HTTP服务器,监听数字比主端口号高1000的端口,也就是28017端口。这意味着,通过浏览器访问http://localhost:28017,能获取数据库的管理信息。
中止mongod
的运行,只须在运行着服务器的shell中按下Ctrl-C。
要想了解启动和停止MongoDB的更多细节,参见第20章。
2.5 MongoDB shell简介
MongoDB自带JavaScript shell,可在shell中使用命令行与MongoDB实例交互。shell非常有用,通过它可以执行管理操作,检查运行实例,亦或做其他尝试。对MongoDB来说,mongo shell是至关重要的工具,其应用之广泛将体现在本书接下来的部分中。
2.5.1 运行shell
运行mongo
启动shell:
$ mongo
MongoDB shell version: 2.4.0
connecting to: test
>
启动时,shell将自动连接MongoDB服务器,须确保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语句是否完整,如没写完可在下一行接着写。在某行连续三次按下回车键可取消未输入完成的命令,并退回到>-提示符。
2.5.2 MongoDB客户端
能运行任意JavaScript程序听上去很酷,不过shell的真正强大之处在于,它是一个独立的MongoDB客户端。启动时,shell会连到MongoDB服务器的test数据库,并将数据库连接赋值给全局变量db
。这个变量是通过shell访问MongoDB的主要入口点。
如果想要查看db
当前指向哪个数据库,可以使用db
命令:
> db
test
为了方便习惯使用SQL shell的用户,shell还包含一些非JavaScript语法的扩展。这些扩展并不提供额外的功能,而是一些非常棒的语法糖。例如,最重要的操作之一为选择数据库:
> use foobar
switched to db foobar
现在,如果查看db
变量,会发现其正指向foobar数据库:
> db
foobar
因为这是一个JavaScript shell,所以键入一个变量会将此变量的值转换为字符串(即数据库名)并打印出来。
通过db
变量,可访问其中的集合。例如,通过db.baz
可返回当前数据库的baz集合。因为通过shell可访问集合,这意味着,几乎所有数据库操作都可以通过shell完成。
2.5.3 shell中的基本操作
在shell中查看或操作数据会用到4个基本操作:创建、读取、更新和删除(即通常所说的CRUD操作)。
1. 创建
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" : ISODate("2012-08-24T21:12:09.982Z")
}
这个对象是个有效的MongoDB文档,所以可以用insert
方法将其保存到blog
集合中:
> db.blog.insert(post)
这篇文章已被存到数据库中。要查看它可用调用集合的find
方法:
> db.blog.find()
{
"_id" : ObjectId("5037ee4a1084eb3ffeef7228"),
"title" : "My Blog Post",
"content" : "Here's my blog post.",
"date" : ISODate("2012-08-24T21:12:09.982Z")
}
可以看到,我们曾输入的键/值对都已被完整地记录。此外,还有一个额外添加的键"_id"
。"_id"
突然出现的原因将在本章末尾解释。
2. 读取
find
和findOne
方法可以用于查询集合里的文档。若只想查看一个文档,可用findOne
:
> db.blog.findOne()
{
"_id" : ObjectId("5037ee4a1084eb3ffeef7228"),
"title" : "My Blog Post",
"content" : "Here's my blog post.",
"date" : ISODate("2012-08-24T21:12:09.982Z")
}
find
和findOne
可以接受一个查询文档作为限定条件。这样就可以查询符合一定条件的文档。使用find
时,shell会自动显示最多20个匹配的文档,也可获取更多文档。第4章会详细介绍查询相关的内容。
3. 更新
使用update
修改博客文章。update
接受(至少)两个参数:第一个是限定条件(用于匹配待更新的文档),第二个是新的文档。假设我们要为先前写的文章增加评论功能,就需要增加一个新的键,用于保存评论数组。
首先,修改变量post
,增加"comments"
键:
> post.comments = []
[ ]
然后执行update
操作,用新版本的文档替换标题为“My Blog Post”的文章:
> db.blog.update({title : "My Blog Post"}, post)
现在,文档已经有了"comments"
键。再用find
查看一下,可以看到新的键:
> db.blog.find()
{
"_id" : ObjectId("5037ee4a1084eb3ffeef7228"),
"title" : "My Blog Post",
"content" : "Here's my blog post.",
"date" : ISODate("2012-08-24T21:12:09.982Z"),
"comments" : [ ]
}
4. 删除
使用remove
方法可将文档从数据库中永久删除。如果没有使用任何参数,它会将集合内的所有文档全部删除。它可以接受一个作为限定条件的文档作为参数。例如,下面的命令会删除刚刚创建的文章:
> db.blog.remove({title : "My Blog Post"})
现在,集合又是空的了。
2.6 数据类型
本章开始部分介绍了文档的基本概念,现在你已经会启动、运行MongoDB,也会在shell中进行一些操作了。这一节的内容会更加深入。MongoDB支持将多种数据类型作为文档中的值,下面将一一介绍。
2.6.1 基本数据类型
在概念上,MongoDB的文档与JavaScript中的对象相近,因而可认为它类似于JSON。JSON(http://www.json.org)是一种简单的数据表示方式:其规范仅用一段文字就能描述清楚(其官网证明了这点),且仅包含6种数据类型。这样有很多好处:易于理解、易于解析、易于记忆。然而,从另一方面来说,因为只有null
、布尔、数字、字符串、数组和对象这几种数据类型,所以JSON的表达能力有一定的局限。
虽然JSON具备的这些类型已具有很强的表现力,但绝大多数应用(尤其是在与数据库打交道时)都还需要其他一些重要的类型。例如,JSON没有日期类型,这使原本容易的日期处理变得烦人。另外,JSON只有一种数字类型,无法区分浮点数和整数,更别说区分32位和64位数字了。再者,JSON无法表示其他一些通用类型,如正则表达式或函数。
MongoDB在保留JSON基本键/值对特性的基础上,添加了其他一些数据类型。 在不同的编程语言下,这些类型的确切表示有些许差异。下面说明MongoDB支持的其他通用类型,以及如何在文档中使用它们。
null
null
用于表示空值或者不存在的字段:
{"x" : null}
- 布尔型
布尔类型有两个值true
和false
:
{"x" : true}
- 数值
shell默认使用64位浮点型数值。因此,以下数值在shell中是很“正常”的:
{"x" : 3.14}
或:
{"x" : 3}
对于整型值,可使用NumberInt
类(表示4字节带符号整数)或NumberLong
类(表示8字符带符号整数),分别举例如下:
{"x" : NumberInt("3")}
{"x" : NumberLong("3")}
- 字符串
UTF-8字符串都可表示为字符串类型的数据:
{"x" : "foobar"}
- 日期
日期被存储为自新纪元以来经过的毫秒数,不存储时区:
{"x" : new Date()}
- 正则表达式
查询时,使用正则表达式作为限定条件,语法也与JavaScript的正则表达式语法相同:
{"x" : /foobar/i}
- 数组
数据列表或数据集可以表示为数组:
{"x" : ["a", "b", "c"]}
- 内嵌文档
文档可嵌套其他文档,被嵌套的文档作为父文档的值:
{"x" : {"foo" : "bar"}}
- 对象id
对象id是一个12字节的ID,是文档的唯一标识。详见2.6.5节。
{"x" : ObjectId()}
还有一些不那么常用,但可能有需要的类型,包括下面这些。
- 二进制数据
二进制数据是一个任意字节的字符串。它不能直接在shell中使用。如果要将非UTF-8字符保存到数据库中,二进制数据是唯一的方式。
- 代码
查询和文档中可以包括任意JavaScript代码:
{"x" : function() { /* ... */ }}
另外,有几种大多数情况下仅在内部使用(或被其他类型取代)的类型。在本书中,出现这种情况时会特别说明。
关于MongoDB数据格式的更多信息,参考附录B。
2.6.2 日期
在JavaScript中,Date
类可以用作MongoDB的日期类型。创建日期对象时,应使用new Date(…)
,而非Date(…)
。如将构造函数(constructor)作为函数进行调用(即不包括new
的方式),返回的是日期的字符串表示,而非日期(Date
)对象。这个结果与MongoDB无关,是JavaScript的工作机制决定的。如果不注意这一点,没有始终使用日期(Date
)构造函数,将得到一堆混乱的日期对象和日期的字符串。由于日期和字符串之间无法匹配,所以执行删除、更新及查询等几乎所有操作时会导致很多问题。
关于JavaScript日期类的完整解释,以及构造函数的参数格式,参见ECMAScript规范15.9节(http://www.ecmascript.org)。
shell根据本地时区设置显示日期对象。然而,数据库中存储的日期仅为新纪元以来的毫秒数,并未存储对应的时区。(当然,可将时区信息存储为另一个键的值)。
2.6.3 数组
数组是一组值,它既能作为有序对象(如列表、栈或队列),也能作为无序对象(如数据集)来操作。
在下面的文档中,"things"
这个键的值是一个数组:
{"things" : ["pie", 3.14]}
此例表示,数组可包含不同数据类型的元素(在此,是一个字符串和一个浮点数)。实际上,常规的键/值对支持的所有值都可以作为数组的值,数组中甚至可以套嵌数组。
文档中的数组有个奇妙的特性,就是MongoDB能“理解”其结构,并知道如何“深入”数组内部对其内容进行操作。这样就能使用数组内容对数组进行查询和构建索引了。例如,之前的例子中,MongoDB可以查询出"things"
数组中包含3.14这个元素的所有文档。要是经常使用这个查询,可以对"things"
创建索引来提高性能。
MongoDB可以使用原子更新对数组内容进行修改,比如深入数组内部将pie改为pi。本书后面还会介绍更多这种操作的例子。
2.6.4 内嵌文档
文档可以作为键的值,这样的文档就是内嵌文档。使用内嵌文档,可以使数据组织方式更加自然,不用非得存成扁平结构的键/值对。
例如,用一个文档来表示一个人,同时还要保存他的地址,可以将地址信息保存在内嵌的"address"
文档中:
{
"name" : "John Doe",
"address" : {
"street" : "123 Park Street",
"city" : "Anytown",
"state" : "NY"
}
}
上面例子中"address"
键的值是一个内嵌文档,这个文档有自己的"street"
、"city"
和"state"
键以及对应的值。
同数组一样,MongoDB能够“理解”内嵌文档的结构,并能“深入”其中构建索引、执行查询或者更新。
稍后会深入讨论模式设计,但是从这个简单的例子也可以看得出内嵌文档可以改变处理数据的方式。在关系型数据库中,这个例子中的文档一般会被拆分成两个表中的两个行 (“people”和“address”各一行)。在MongoDB中,就可以直接将地址文档嵌入到人员文档中。使用得当的话,内嵌文档会使信息的表示方式更加自然(通常也会更高效)。
MongoDB这样做的坏处就是会导致更多的数据重复。假设“address”是关系数据库中的一个独立的表,我们需要修正地址中的拼写错误。当我们对“people”和“address”执行连接操作时,使用这个地址的每个人的信息都会得到更新。但是在MongoDB中,则需要对每个人的文档分别修正拼写错误。
2.6.5 _id和ObjectId
MongoDB中存储的文档必须有一个"_id"
键。这个键的值可以是任何类型的,默认是个ObjectId
对象。在一个集合里面,每个文档都有唯一的"_id"
,确保集合里面每个文档都能被唯一标识。如果有两个集合的话,两个集合可以都有一个"_id"
的值为123,但是每个集合里面只能有一个文档的"_id"
值为123。
1. ObjectId
ObjectId
是"_id"
的默认类型。它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。这是 MongoDB采用ObjectId
,而不是其他比较常规的做法(比如自动增加的主键)的主要原因,因为在多个服务器上同步自动增加主键值既费力又费时。因为设计MongoDB的初衷就是用作分布式数据库,所以能够在分片环境中生成唯一的标示符非常重要。
ObjectId
使用12字节的存储空间,是一个由24个十六进制数字组成的字符串(每个字节可以存储两个十六进制数字)。由于看起来很长,不少人会觉得难以处理。但关键是要知道这个长长的ObjectId
是实际存储数据的两倍长。
如果快速连续创建多个ObjectId
,会发现每次只有最后几位数字有变化。另外,中间的几位数字也会变化(要是在创建的过程中停顿几秒钟)。这是ObjectId
的创建方式导致的。ObjectId
的12字节按照如下方式生成:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11
时间戳 | 机器 | PID | 计数器
ObjectId
的前4个字节是从标准纪元开始的时间戳,单位为秒。这会带来一些有用的属性。
- 时间戳,与随后的5字节(稍后介绍)组合起来,提供了秒级别的唯一性。
- 由于时间戳在前,这意味着
ObjectId
大致会按照插入的顺序排列。这对于某些方面很有用,比如可以将其作为索引提高效率,但是这个是没有保证的,仅仅是“大致”。 - 这4字节也隐含了文档创建的时间。绝大多数驱动程序都会提供一个方法,用于从
ObjectId
获取这些信息。
因为使用的是当前时间,很多用户担心要对服务器进行时钟同步。虽然在某些情况下,在服务器间进行时间同步确实是个好主意(参见23.6.1节),但是这里其实没有必要,因为时间戳的实际值并不重要,只要它总是不停增加就好了(每秒一次)。
接下来的3字节是所在主机的唯一标识符。通常是机器主机名的散列值(hash)。这样就可以确保不同主机生成不同的 ObjectId
,不产生冲突。
为了确保在同一台机器上并发的多个进程产生的ObjectId
是唯一的,接下来的两字节来自产生ObjectId
的进程的进程标识符(PID)。
前9字节保证了同一秒钟不同机器不同进程产生的ObjectId
是唯一的。最后3字节是一个自动增加的计数器,确保相同进程同一秒产生的ObjectId
也是不一样的。一秒钟最多允许每个进程拥有2563(16 777 216)个不同的ObjectId
。
2. 自动生成_id
前面讲到,如果插入文档时没有"_id"
键,系统会自动帮你创建一个。可以由MongoDB服务器来做这件事,但通常会在客户端由驱动程序完成。这一做法非常好地体现了MongoDB的哲学:能交给客户端驱动程序来做的事情就不要交给服务器来做。这种理念背后的原因是,即便是像MongoDB这样扩展性非常好的数据库,扩展应用层也要比扩展数据库层容易得多。将工作交由客户端来处理,就减轻了数据库扩展的负担。
2.7 使用MongoDB Shell
本节将介绍如何将shell作为命令行工具的一部分来使用,如何对shell进行定制,以及shell的一些高级功能。
在上面的例子中,我们只是连接到了一个本地的mongod实例。事实上,可以将shell连接到任何MongoDB实例(只要你的计算机与MongoDB实例所在的计算机能够连通)。在启动shell时指定机器名和端口,就可以连接到一台不同的机器(或者端口):
$ mongo some-host:30000/myDB
MongoDB shell version: 2.4.0
connecting to: some-host:30000/myDB
>
db
现在就指向了some-host:30000上的myDB数据库。
启动mongo shell时不连接到任何mongod有时很方便。通过--nodb
参数启动shell,启动时就不会连接任何数据库:
$ mongo --nodb
MongoDB shell version: 2.4.0
>
启动之后,在需要时运行new Mongo(hostname)
命令就可以连接到想要的mongod了:
> conn = new Mongo("some-host:30000")
connection to some-host:30000
> db = conn.getDB("myDB")
myDB
执行完这些命令之后,就可以像平常一样使用db
了。任何时候都可以使用这些命令来连接到不同的数据库或者服务器。
2.7.1 shell小贴士
由于mongo
是一个简化的JavaScript shell,可以通过查看JavaScript的在线文档得到大量帮助。对于MongoDB特有的功能,shell内置了帮助文档,可以使用help
命令查看:
> help
db.help() help on db methods
db.mycoll.help() help on collection methods
sh.help() sharding helpers
...
show dbs show database names
show collections show collections in current database
show users show users in current database
...
可以通过db.help()
查看数据库级别的帮助,使用db.foo.help()
查看集合级别的帮助。
如果想知道一个函数是做什么用的,可以直接在shell输入函数名(函数名后不要输入小括号),这样就可以看到相应函数的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);
}
2.7.2 使用shell执行脚本
本书其他章都是以交互方式使用shell,但是也可以将希望执行的JavaScript文件传给shell。直接在命令行中传递脚本就可以了:
$ mongo script1.js script2.js script3.js
MongoDB shell version: 2.4.0
connecting to: test
I am script1.js
I am script2.js
I am script3.js
$
mongo shell会依次执行传入的脚本,然后退出。
如果希望使用指定的主机/端口上的mongod运行脚本,需要先指定地址,然后再跟上脚本文件的名称:
$ mongo --quiet server-1:30000/foo script1.js script2.js script3.js
这样可以将db
指向server-1:30000上的foo数据库,然后执行这三个脚本。如上所示,运行shell时指定的命令行选项要出现在地址之前。
可以在脚本中使用print()
函数将内容输出到标准输出(stdout),如上面的脚本所示。这样就可以在shell中使用管道命令。如果将shell脚本的输出管道给另一个使用--quiet
选项的命令,就可以让shell不打印“MongoDB shell version…”提示。
也可以使用load()
函数,从交互式shell中运行脚本:
> load("script1.js")
I am script1.js
>
在脚本中可以访问db
变量,以及其他全局变量。然而,shell辅助函数(比如"use db"
和"show collections"
)不可以在文件中使用。这些辅助函数都有对应的JavaScript函数,如表2-1所示。
表2-1 shell辅助函数对应的JavaScript函数
辅助函数 | 等价函数 |
---|---|
use foo | db.getSisterDB("foo") |
show dbs | db.getMongo().getDBs() |
show collections | db.getCollectionNames() |
可以使用脚本将变量注入到shell。例如,可以在脚本中简单地初始化一些常用的辅助函数。例如,下面的脚本对于本书的复制和分片部分内容非常有用。这个脚本定义了一个connectTo()
函数,它连接到指定端口处的一个本地数据库,并且将db
指向这个连接。
// defineConnectTo.js
/**
* 连接到指定的数据库,并且将db指向这个连接
*/
var connectTo = function(port, dbname) {
if (!port) {
port = 27017;
}
if (!dbname) {
dbname = "test";
}
db = connect("localhost:"+port+"/"+dbname);
return db;
};
如果在shell中加载这个脚本,connectTo
函数就可以使用了。
> typeof connectTo
undefined
> load('defineConnectTo.js')
> typeof connectTo
function
除了添加辅助函数,还可以使用脚本将通用的任务和管理活动自动化。
默认情况下,shell会在运行shell时所处的目录中查找脚本(可以使用run("pwd")
命令查看)。如果脚本不在当前目录中,可以为shell指定一个相对路径或者绝对路径。例如,如果脚本放置在~/my-scripts目录中,可以使用load("/home/myUser/my-scripts/defineConnectTo.js")
命令来加载defineConnectTo.js。注意,load
函数无法解析~符号。
也可以在shell中使用run()
函数来执行命令行程序。可以在函数参数列表中指定程序所需的参数:
> run("ls", "-l", "/home/myUser/my-scripts/")
sh70352| -rw-r--r-- 1 myUser myUser 2012-12-13 13:15 defineConnectTo.js
sh70532| -rw-r--r-- 1 myUser myUser 2013-02-22 15:10 script1.js
sh70532| -rw-r--r-- 1 myUser myUser 2013-02-22 15:12 script2.js
sh70532| -rw-r--r-- 1 myUser myUser 2013-02-22 15:13 script3.js
通常来说,这种使用方式的局限性非常大,因为输出格式很奇怪,而且不支持管道。
2.7.3 创建.mongorc.js文件
如果某些脚本会被频繁加载,可以将它们添加到mongorc.js文件中。这个文件会在启动shell时自动运行。
例如,我们希望启动成功时让shell显示一句欢迎语。为此,我们在用户主目录下创建一个名为.mongorc.js的文件,向其中添加如下内容:
// mongorc.js
var compliment = ["attractive", "intelligent", "like Batman"];
var index = Math.floor(Math.random()*3);
print("Hello, you're looking particularly "+compliment[index]+" today!");
然后,当启动shell时,就会看到这样一些内容:
$ mongo
MongoDB shell version: 2.4.0-
preconnecting to: test
Hello, you're looking particularly like Batman today!
>
为了实用,可以使用这个脚本创建一些自己需要的全局变量,或者是为太长的名字创建一个简短的别名,也可以重写内置的函数。.mongorc.js最见的用途之一是移除那些比较“危险”的shell辅助函数。可以在这里集中重写这些方法,比如为dropDatabase
或者deleteIndexes
等辅助函数添加no
选项,或者取消它们的定义。
var no = function() {
print("Not on my watch.");
};
// 禁止删除数据库
db.dropDatabase = DB.prototype.dropDatabase = no;
// 禁止删除集合
DBCollection.prototype.drop = no;
// 禁止删除索引
DBCollection.prototype.dropIndex = no;
改变数据库函数时,要确保同时对db
变量和DB
原型进行改变(如上例所示)。如果只改变了其中一个,那么db
变量可能没有改变,或者这些改变在新使用的所有数据库(运行use anotherDB
命令)中都不会生效。
现在,如果试图调用这些函数,就会得到一条错误提示。注意,这种方式并不能保护数据库免受恶意用户的攻击,只能预防自己的手误。
如果在启动shell时指定--norc
参数,就可以禁止加载.mongorc.js。
2.7.4 定制shell提示
将prompt
变量设为一个字符串或者函数,就可以重写默认的shell提示。例如,如果正在运行一个需要耗时几分钟的查询,你可能希望完成时在shell提示中输出当前时间,这样就可以知道最后一个操作的完成时间了。
prompt = function() {
return (new Date())+"> ";
};
另一个方便的提示是显示当前使用的数据库:
prompt = function() {
if (typeof db == 'undefined') {
return '(nodb)> ';
}
// 检查最后的数据库操作
try {
db.runCommand({getLastError:1});
}
catch (e) {
print(e);
}
return db+"> ";
};
注意,提示函数应该返回字符串,而且应该小心谨慎地处理异常:如果提示中出现了异常会对用户造成困惑!
通常来说,提示函数中应该包含对getLastError
的调用。这样可以捕获数据库错误,而且可以在shell断开时自动重新连接(比如重启了mongod)。
可以在.mongorc.js中定制自己想要的提示。也可以定制多个提示,在shell中可以自由切换。
2.7.5 编辑复合变量
shell的多行支持是非常有限的:不可以编辑之前的行。如果编辑到第15行时发现第1行有个错误,那会让人非常懊恼。因此,对于大块的代码或者是对象,你可能更愿意在编辑器中编辑。为了方便地调用编辑器,可以在shell中设置EDITOR
变量(也可以在环境变量中设置):
> EDITOR="/usr/bin/emacs"
现在,如果想要编辑一个变量,可以使用"edit 变量名"
这个命令,比如:
> var wap = db.books.findOne({title: "War and Peace"})
> edit wap
修改完成之后,保存并退出编辑器。变量就会被重新解析然后加载回shell。
在.mongorc.js文件中添加一行内容,EDITOR="编辑器路径";
,以后就不必单独设置EDITOR
变量了。
2.7.6 集合命名注意事项
可以使用db.collectionName
获取一个集合的内容,但是,如果集合名称中包含保留字或者无效的JavaScript属性名称,db.collectionName
就不能正常工作了。
假设要访问version集合,不能直接使用db.version
,因为db.version
是db
的一个方法(会返回当前MongoDB服务器的版本):
> db.version
function () {
return this.serverBuildInfo().version;
}
为了访问version集合,必须使用getCollection
函数:
> db.getCollection("version");
test.version
如果集合名称中包含无效的JavaScript属性名称(比如foo-bar-baz和123abc),也可以使用这个函数来访问相应的集合。(注意,JavaScript属性名称只能包含字母、数字,以及"$"
和"_"
字符,而且不能以数字开头。)
还有一种方法可以访问以无效属性名称命名的集合,那就是使用数组访问语法:在JavaScript中,x.y
等同于x['y']
。也就是说,除了名称的字面量之外,还可以使用变量访问子集合。因此,如果需要对blog的每一个子集合进行操作,可以使用如下方式进行迭代:
var collections = ["posts", "comments", "authors"];
for (var i in collections) {
print(db.blog[collections[i]]);
}
而不必这样:
print(db.blog.posts);
print(db.blog.comments);
print(db.blog.authors);
注意,不能使用db.blog.i,这样会被解释为test.blog.i,而不是test.blog.posts。必须使用db.blog[i]
语法才能将i
解释为相应的变量。
可以使用这种方式来访问那些名字怪异的集合:
> var name = "@#&!"
> db[name].find()
直接使用db.@#&!
进行查询是非法的,但是可以使用db[name]
。