本篇文章总结自《MongoDB 权威指南》,MongoDB 的安装可参考:
MongoDB 非常强大但很容易上手。本章会介绍一些 MongoDB 的基本概念。
- 文档是 MongoDB 中数据的基本单元,非常类似于关系型数据库管理系统中的行,但更具表现力。
- 类似地, 集合(collection)可以看作是一个拥有动态模式(dynamic schema)的表。
- MongoDB 的一个实例可以拥有多个相互独立的数据库(database),每一个数据库都拥有自己的集合。
- 每一个文档都有一个特殊的键
_id, 这个键在文档所属的集合中是唯一的。 - MongoDB 自带了一个简单但功能强大的 JavaScript shell,可用于管理 MongoDB 的实例或数据操作。
文档
文档是 MongoDB 的核心概念。文档就是键值对的一个有序集。每种编程语言表示文档的方法不太一样,但大多数编程语言都有一些相通的数据结构,比如映射
(map)、散列(hash)或字典(dictionary)。例如,在 JavaScript 里面,文档被表示为对象:
{"greeting" : "Hello MongoDB!"}
这个文档只有一个键 greeting,其对应的值为 Hello, world!。大多数文档会比这个简单的例子复杂得多,通常会包含多个键/值对:
{"greeting" : "Hello MongoDB!", "foo": 3}
从上面的例子可以看出,文档中的值可以是多种不同的数据类型(甚至可以是一个完整的内嵌文档)。在这个例子中, 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 中的散列)。通常,这些语言的驱动具有某些特殊的机制,可以在必要时指定文档的顺序。
集合
集合就是一组文档。如果将 MongoDB 中的一个文档比喻为关系型数据库中的一行,那么一个集合就相当于一张表。
动态模式
集合是动态模式的。这意味着一个集合里面的文档可以是各式各样的。例如,下面两个文档可以存储在同一个集合里面:
{"greeting" : "Hello, world!"}
{"foo": 5}
需要注意的是,上面的文档不光值的类型不同(一个是字符串,一个是整数),它们的键也完全不同。因为集合里面可以放置任何文档,随之而来的一个问题是:还有必要使用多个集合吗?这的确值得思考:既然没有必要区分不同类型文档的模式,为什么还要使用多个集合呢?这里有几个重要的原因。
-
如果把各种各样的文档不加区分地放在同一个集合里,无论对开发者还是对管理员来说都将是噩梦。开发者要么确保每次查询只返回特定类型的文档,要么让执行查询的应用程序来处理所有不同类型的文档。如果查询博客文章时还要剔除含有作者数据的文档,这会带来很大困扰。
-
在一个集合里查询特定类型的文档在速度上也很不划算,分开查询多个集合要快得多。例如,假设集合里面一个名为
type的字段用于指明文档是skim、
whole还是chunky monkey。那么,如果从一个集合中查询这三种类型的文档,速度会很慢。但如果将这三种不同类型的文档拆分为三个不同的集合,每次只需要查询相应的集合,速度快得多。 -
把同种类型的文档放在一个集合里,数据会更加集中。从一个只包含博客文章的集合里查询几篇文章,或者从同时包含文章数据和作者数据的集合里查出几篇文章,相比之下,前者需要的磁盘寻道操作更少。
-
创建索引时,需要使用文档的附加结构(特别是创建唯一索引时)。索引是按照集合来定义的。在一个集合中只放入一种类型的文档,可以更有效地对集合进行索引。
上面这些重要原因促使我们创建一个模式,把相关类型的文档组织在一起,尽管 MongoDB 对此并没有强制要求。
命名
集合使用名称进行标识。集合名可以是满足下列条件的任意 UTF-8 字符串。
- 集合名不能是空字符串(
"")。 - 集合名不能包含
\0字符(空字符),这个字符表示集合名的结束。 - 集合名不能以
system.开头,这是为系统集合保留的前缀。例如,system.users这个集合保存着数据库的用户信息,而system.namespaces集合保存着所有数据库集合的信息。 - 用户创建的集合不能在集合名中包含保留字符
$。因为某些系统生成的集合中包含$,很多驱动程序确实支持在集合名里包含该字符。除非你要访问这种系统
创建的集合,否则不应该在集合名中包含$。
子集合
组织集合的一种惯例是使用 . 分隔不同命名空间的子集合。例如,一个具有博客功能的应用可能包含两个集合,分别是 blog.posts 和 blog.authors。这是为了使组织结构更清晰,这里的 blog 集合(这个集合甚至不需要存在)跟它的子集合没有任何关系。
虽然子集合没有任何特别的属性,但它们却非常有用,因而很多 MongoDB 工具都使用了子集合。
- GridFS(一种用于存储大文件的协议)使用子集合来存储文件的元数据,这样就可以与文件内容块很好地隔离开来。
- 大多数驱动程序都提供了一些语法糖,用于访问指定集合的子集合。例如,在数据库 shell 中,
db.blog代表blog集合,而db.blog.posts代表blog.posts集合。
在 MongoDB 中,使用子集合来组织数据非常高效,值得推荐。
数据库
在 MongoDB 中,多个文档组成集合,而多个集合可以组成数据库。一个 MongoDB 实例可以承载多个数据库,每个数据库拥有 0 个或者多个集合。每个数据库都有独立的权限,即便是在磁盘上,不同的数据库也放置在不同的文件中。按照经验,我们将有关一个应用程序的所有数据都存储在同一个数据库中。要想在同一
个 MongoDB 服务器上存放多个应用程序或者用户的数据,就需要使用不同的数据库。
数据库通过名称来标识,这点与集合类似。数据库名可以是满足以下条件的任意 UTF-8 字符串。
- 不能是空字符串(
"")。 - 不得含有
/、\、.、"、*、<、>、:、|、?、$(一个空格)、\0(空字符)。基本上,只能使用 ASCII 中的字母和数字。 - 数据库名区分大小写,即便是在不区分大小写的文件系统中也是如此。简单起见,数据库名应全部小写。
- 数据库名最多为 64 字节。
要记住一点,数据库最终会变成文件系统里的文件,而数据库名就是相应的文件名,这是数据库名有如此多限制的原因。
另外,有一些数据库名是保留的,可以直接访问这些有特殊语义的数据库。这些数据库如下所示。
admin
从身份验证的角度来讲,这是root数据库。如果将一个用户添加到admin数据库,这个用户将自动获得所有数据库的权限。再者,一些特定的服务器端命令也只能从admin数据库运行,如列出所有数据库或关闭服务器。local
这个数据库永远都不可以复制,且一台服务器上的所有本地集合都可以存储在这个数据库中。config
MongoDB 用于分片设置时,分片信息会存储在config数据库中。把数据库名添加到集合名前,得到集合的完全限定名,即命名空间(namespace)。
例如,如果要使用cms数据库中的blog.posts集合,这个集合的命名空间就是cms.blog.posts。命名空间的长度不得超过 121 字节,且在实际使用中应小于 100 字节。
Mongo shell 简介
MongoDB 自带 JavaScript shell,可在 shell 中使用命令行与 MongoDB 实例交互。shell 非常有用,通过它可以执行管理操作,检查运行实例,亦或做其他尝试。对 MongoDB 来说, mongo shell 是至关重要的工具。
运行 shell
运行 mongo 启动 shell:
$ mongo
MongoDB shell version v4.4.2
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("27bee91d-75aa-4264-a39e-5733d25d191b") }
MongoDB server version: 4.4.2
---
...
>
启动时, shell 将自动连接 MongoDB 服务器,须确保 mongod 已启动 。
shell 是一个功能完备的 JavaScript 解释器,可运行任意 JavaScript 程序。为说明这一点,我们运行几个简单的数学运算:
> x=100
100
> x/2
50
另外,可充分利用 JavaScript 的标准库:
> Math.sin(Math.PI/2)
1
> new Date("2020/2/2")
ISODate("2020-02-02T00:00:00Z")
> "Hello, World!".replace("World", "MongoDB")
Hello, MongoDB!
再者,可定义和调用 JavaScript 函数:
> function test(n) {
... if (n <= 1) return 1;
... return n * test(n - 1);
... }
> test(5)
120
需要注意,可使用多行命令。 shell 会检测输入的 JavaScript 语句是否完整,如没写完可在下一行接着写。在某行连续三次按下回车键可取消未输入完成的命令,并退回到 > 提示符。
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 完成。
shell 中的基本操作
在 shell 中查看或操作数据会用到 4 个基本操作:创建、读取、更新和删除(即通常所说的 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" : ISODate("2020-12-29T06:03:36.543Z")
}
这个对象是个有效的 MongoDB 文档,所以可以用 insert 方法将其保存到 blog 集合中:
> db.blog.insert(post)
WriteResult({ "nInserted" : 1 })
这篇文章已被存到数据库中。要查看它可用调用集合的 find 方法:
> db.blog.find();
{ "_id" : ObjectId("5feac7140479823734ebe11d"), "title" : "My Blog Post", "content" : "Here's my blog post.", "date" : ISODate("2020-12-29T06:03:36.543Z") }
可以看到,我们曾输入的键 / 值对都已被完整地记录。此外,还有一个额外添加的键 "_id"。
读取
find 和 findOne 方法可以用于查询集合里的文档。若只想查看一个文档,可用 findOne:
> db.blog.findOne();
{
"_id" : ObjectId("5feac7140479823734ebe11d"),
"title" : "My Blog Post",
"content" : "Here's my blog post.",
"date" : ISODate("2020-12-29T06:03:36.543Z")
}
find 和 findOne 可以接受一个查询文档作为限定条件。这样就可以查询符合一定条件的文档。使用 find 时, shell 会自动显示最多 20 个匹配的文档。
更新
使用 update 修改博客文章。update 接受(至少)两个参数:第一个是限定条件(用于匹配待更新的文档),第二个是新的文档。假设我们要为先前写的文章增加评论功能,就需要增加一个新的键,用于保存评论数组。
首先,修改变量 post,增加 "comments" 键:
> post.comments = []
[ ]
然后执行 update 操作,用新版本的文档替换标题为 My Blog Post 的文章:
> db.blog.update({"title": "My Blog Post"}, post)
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
现在,文档已经有了 "comments" 键。再用 find 查看一下,可以看到新的键:
> db.blog.find()
{ "_id" : ObjectId("5feac7140479823734ebe11d"), "title" : "My Blog Post", "content" : "Here's my blog post.", "date" : ISODate("2020-12-29T06:03:36.543Z"), "comments" : [ ] }
删除
使用 remove 方法可将文档从数据库中永久删除。如果没有使用任何参数,它会将集合内的所有文档全部删除。它可以接受一个作为限定条件的文档作为参数。例如,下面的命令会删除刚刚创建的文章:
> db.blog.remove({"title": "My Blog Post"})
WriteResult({ "nRemoved" : 1 })
现在,集合又是空的了。
数据类型
MongoDB 支持将多种数据类型作为文档中的值,下面将一一介绍。
基本数据类型
在概念上, 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,是文档的唯一标识。如:{"x" : ObjectId()}。
还有一些不那么常用,但可能有需要的类型,包括下面这些。
- 二进制数据
二进制数据是一个任意字节的字符串。它不能直接在 shell 中使用。如果要将非 UTF-8 字符保存到数据库中,二进制数据是唯一的方式。 - 代码
查询和文档中可以包括任意 JavaScript 代码:{"x" : function() { /* ... */ }}。
日期
在 JavaScript 中, Date 类可以用作 MongoDB 的日期类型。创建日期对象时,应使用 new Date(...),而非 Date(…)。如将构造函数(constructor)作为函数进行调用(即不包括 new 的方式),返回的是日期的字符串表示,而非日期( Date)对象。
这个结果与 MongoDB 无关,是 JavaScript 的工作机制决定的。如果不注意这一点,没有始终使用日期(Date)构造函数,将得到一堆混乱的日期对象和日期的字符串。由于日期和字符串之间无法匹配,所以执行删除、更新及查询等几乎所有操作时会导致很多问题。
关于 JavaScript 日期类的完整解释,以及构造函数的参数格式,参见 ECMAScript 规范 15.9 节(http://www.ecmascript.org)。
shell 根据本地时区设置显示日期对象。然而,数据库中存储的日期仅为新纪元以来的毫秒数,并未存储对应的时区。(当然,可将时区信息存储为另一个键的值)。
数组
数组是一组值,它既能作为有序对象(如列表、栈或队列),也能作为无序对象(如数据集)来操作。
在下面的文档中, "things 这个键的值是一个数组:
{"things" : ["pie", 3.14]}
此例表示,数组可包含不同数据类型的元素(在此,是一个字符串和一个浮点数)。
实际上,常规的键/值对支持的所有值都可以作为数组的值,数组中甚至可以套嵌数组。
文档中的数组有个奇妙的特性,就是 MongoDB 能“理解”其结构,并知道如何 “深入”数组内部对其内容进行操作。这样就能使用数组内容对数组进行查询和构建
索引了。例如,之前的例子中, MongoDB 可以查询出 "things" 数组中包含 3.14 这个元素的所有文档。要是经常使用这个查询,可以对 "things" 创建索引来提高性能。
MongoDB 可以使用原子更新对数组内容进行修改,比如深入数组内部将 pie 改为 pi。
内嵌文档
文档可以作为键的值,这样的文档就是内嵌文档。使用内嵌文档,可以使数据组织方式更加自然,不用非得存成扁平结构的键/值对。
例如,用一个文档来表示一个人,同时还要保存他的地址,可以将地址信息保存在内嵌的 "address" 文档中:
{
"name" : "John Doe",
"address" : {
"street" : "123 Park Street",
"city" : "Anytown",
"state" : "NY"
}
}
上面例子中 "address" 键的值是一个内嵌文档,这个文档有自己的 "street"、"city" 和 "state" 键以及对应的值。
同数组一样, MongoDB 能够“理解”内嵌文档的结构,并能“深入”其中构建索引、执行查询或者更新。
从这个简单的例子也可以看得出内嵌文档可以改变处理数据的方式。在关系型数据库中,这个例子中的文档一般会被拆分成两个表中的两个行。在 MongoDB 中,就可以直接将地址文档嵌入到人员文档中。使用得当的话,内嵌文档会使信息的表示方式更加自然(通常也会更高效)。
当然,MongoDB 这样做的坏处就是会导致更多的数据重复。
_id 和 ObjectId
MongoDB 中存储的文档必须有一个 _id 键。这个键的值可以是任何类型的,默认是个 ObjectId 对象。在一个集合里面,每个文档都有唯一的 _id,确保集合里面每个文档都能被唯一标识。如果有两个集合的话,两个集合可以都有一个 _id 的值为 123,但是每个集合里面只能有一个文档的 _id 值为 123。
ObjectId
ObjectId 是 _id 的默认类型。它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。这是 MongoDB 采用 ObjectId,而不是其他比较常规的做法(比如自动增加的主键)的主要原因,因为在多个服务器上同步自动增加主键值既费力又费时。因为设计 MongoDB 的初衷就是用作分布式数据库,所以能够在分片环境中生成唯一的标示符非常重要。
ObjectId 使用 12 字节的存储空间,是一个由 24 个十六进制数字组成的字符串(每个字节可以存储两个十六进制数字)。由于看起来很长,不少人会觉得难以处理。但关键是要知道这个长长的 ObjectId 是实际存储数据的两倍长。
如果快速连续创建多个 ObjectId,会发现每次只有最后几位数字有变化。另外,中间的几位数字也会变化(要是在创建的过程中停顿几秒钟)。这是 ObjectId 的创建方式导致的。 ObjectId 的 12 字节按照如下方式生成:

ObjectId 的前 4 个字节是从标准纪元开始的时间戳,单位为秒。这会带来一些有用的属性。
- 时间戳,与随后的 5 字节(稍后介绍)组合起来,提供了秒级别的唯一性。
- 由于时间戳在前,这意味着
ObjectId大致会按照插入的顺序排列。这对于某些方面很有用,比如可以将其作为索引提高效率,但是这个是没有保证的,仅仅是“大致”。 - 这 4 字节也隐含了文档创建的时间。绝大多数驱动程序都会提供一个方法,用于从
ObjectId获取这些信息。
因为使用的是当前时间,很多用户担心要对服务器进行时钟同步。虽然在某些情况下,在服务器间进行时间同步确实是个好主意,但是这里其实没有必要,因为时间戳的实际值并不重要,只要它总是不停增加就好了(每秒一次)。
接下来的 3 字节是所在主机的唯一标识符。通常是机器主机名的散列值(hash)。这样就可以确保不同主机生成不同的 ObjectId,不产生冲突。
为了确保在同一台机器上并发的多个进程产生的 ObjectId 是唯一的,接下来的两字节来自产生 ObjectId 的进程的进程标识符(PID)。
前 9 字节保证了同一秒钟不同机器不同进程产生的 ObjectId 是唯一的。最后 3 字节是一个自动增加的计数器,确保相同进程同一秒产生的 ObjectId 也是不一样的。一秒钟最多允许每个进程拥有 256^3(16 777 216)个不同的 ObjectId。
自动生成 _id
前面讲到,如果插入文档时没有 _id 键,系统会自动帮你创建一个。可以由MongoDB 服务器来做这件事,但通常会在客户端由驱动程序完成。这一做法非常
好地体现了 MongoDB 的哲学:能交给客户端驱动程序来做的事情就不要交给服务器来做。这种理念背后的原因是,即便是像 MongoDB 这样扩展性非常好的数据库,扩展应用层也要比扩展数据库层容易得多。将工作交由客户端来处理,就减轻了数据库扩展的负担。
使用 Mongo shell
在上面的例子中,我们只是连接到了一个本地的 mongod 实例。事实上,可以将 shell 连接到任何 MongoDB 实例(只要你的计算机与 MongoDB 实例所在的计算机能够连通)。在启动 shell 时指定机器名和端口,就可以连接到一台不同的机器(或者端口):
$ mongo 10.0.1.111:27017/admin
db 现在就指向了 10.0.1.111:27017 上的 admin 数据库。
启动 mongo shell 时不连接到任何 mongod 有时很方便。通过--nodb 参数启动 shell,启动时就不会连接任何数据库
$ mongo --nodb
MongoDB shell version v4.4.2
>
启动之后, 在需要时运行 new Mongo(hostname) 命令就可以连接到想要的 mongod 了:
> conn = new Mongo("localhost:27017")
connection to localhost:27017
> db = conn.getDB("admin")
admin
> db
admin
执行完这些命令之后,就可以像平常一样使用 db 了。任何时候都可以使用这些命令来连接到不同的数据库或者服务器。
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
rs.help() replica set helpers
help admin administrative help
help connect connecting to a db help
...
可以通过 db.help() 查看数据库级别的帮助,使用 db.foo.help() 查看集合级别的帮助。
如果想知道一个函数是做什么用的,可以直接在 shell 输入函数名(函数名后不要输入小括号),这样就可以看到相应函数的 JavaScript 实现代码。例如,如果想知道 update 函数的工作机制,或者是记不清参数的顺序,就可以像下面这样做:
> db.foo.update
function(query, updateSpec, upsert, multi) {
var parsed = this._parseUpdate(query, updateSpec, upsert, multi);
var query = parsed.query;
var updateSpec = parsed.updateSpec;
const hint = parsed.hint;
...
使用 shell 执行脚本
本书其他章都是以交互方式使用 shell,但是也可以将希望执行的 JavaScript 文件传给 shell。直接在命令行中传递脚本就可以了:
$ mongo script1.js script2.js script3.js
mongo shell 会依次执行传入的脚本,然后退出。
如果希望使用指定的主机/端口上的 mongod 运行脚本,需要先指定地址,然后再跟上脚本文件的名称:
$ mongo --quiet 10.0.1.111:27017/admin script1.js script2.js script3.js
这样可以将 db 指向 10.0.1.111:27017 上的 admin 数据库,然后执行这三个脚本。如上所示,运行 shell 时指定的命令行选项要出现在地址之前。
可以在脚本中使用 print() 函数将内容输出到标准输出(stdout),如上面的脚本所示。这样就可以在 shell 中使用管道命令。如果将 shell 脚本的输出管道给另一个使用 --quiet 选项的命令,就可以让 shell 不打印 MongoDB shell version… 提示。
也可以使用 load() 函数,从交互式 shell 中运行脚本:
> load("script1.js")
i am in script1.js
true
在脚本中可以访问 db 变量,以及其他全局变量。然而, shell 辅助函数(比如 use db 和 show collections)不可以在文件中使用。这些辅助函数都有对应的 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 函数就可以使用了。
$ mongo --nodb
MongoDB shell version v4.4.2
> load("defineConnectTo.js")
true
> typeof connectTo
function
> connectTo()
connecting to: mongodb://localhost:27017/test
Implicit session: session { "id" : UUID("e02b732b-5526-4221-8ef2-b921dd427417") }
MongoDB server version: 4.4.2
test
>
除了添加辅助函数,还可以使用脚本将通用的任务和管理活动自动化。
默认情况下, shell 会在运行 shell 时所处的目录中查找脚本(可以使用 run("pwd") 命令查看)。如果脚本不在当前目录中,可以为 shell 指定一个相对路径或者绝对路径。例如,如果脚本放置在 ~/my-scripts 目录中,可以使用 load("/home/myUser/my-scripts/defineConnectTo.js") 命令来加载 defineConnectTo.js。 注 意,load 函数无法解析 ~ 符号。
也可以在 shell 中使用 run() 函数来执行命令行程序。可以在函数参数列表中指定程序所需的参数:
> run("ls", "-l", "/opt/apps")
{"t":{"$date":"2020-12-29T07:33:21.933Z"},"s":"I", "c":"-", "id":22810, "ctx":"js","msg":"shell: Started program","attr":{"pid":"44200","argv":["/bin/ls","-l","/opt/apps"]}}
sh44200| total 4
sh44200| drwxr-xr-x 6 mongo mongo 4096 Dec 29 02:23 mongodb
0
通常来说,这种使用方式的局限性非常大,因为输出格式很奇怪,而且不支持管道。
创建 .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 --nodb
MongoDB shell version v4.4.2
Hello, you're looking particularly attractive 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。
定制 shell 提示
将 prompt 变量设为一个字符串或者函数,就可以重写默认的 shell 提示(类似 Linux shell 的 PS1 变量)。例如,如果正在运行一个需要耗时几分钟的查询,你可能希望完成时在 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 中可以自由切换。
编辑复合变量
shell 的多行支持是非常有限的:不可以编辑之前的行。如果编辑到第 15 行时发现第 1 行有个错误,那会让人非常懊恼。因此,对于大块的代码或者是对象,你可能更愿意在编辑器中编辑。为了方便地调用编辑器,可以在 shell 中设置 EDITOR 变量(也可以在环境变量中设置):
> EDITOR="/usr/bin/vim"
现在,如果想要编辑一个变量,可以使用 edit <变量名> 这个命令,比如:
> a = "Hello World!"
Hello World!
> edit a
> a
Hello MongoDB!
修改完成之后,保存并退出编辑器。变量就会被重新解析然后加载回 shell。
在.mongorc.js 文件中添加一行内容, EDITOR="<编辑器路径>";,以后就不必单独设置 EDITOR 变量了。
集合命名注意事项
可以使用 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 = "@#&i"
> db[name].find()
直接使用 db.@#&! 进行查询是非法的,但是可以使用 db[name]。
评论区