侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

MongoDB 基础知识

zze
zze
2020-12-29 / 0 评论 / 0 点赞 / 615 阅读 / 27218 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

本篇文章总结自《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.postsblog.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"

读取

findfindOne 方法可以用于查询集合里的文档。若只想查看一个文档,可用 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")
}

findfindOne 可以接受一个查询文档作为限定条件。这样就可以查询符合一定条件的文档。使用 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}

  • 布尔型
    布尔类型有两个值 truefalse{"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 字节按照如下方式生成:

image.png

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 dbshow collections)不可以在文件中使用。这些辅助函数都有对应的 JavaScript 函数,如下表所示。

辅助函数等价函数
use foodb.getSisterDB("foo")
show dbsdb.getMongo().getDBs()
show collectionsdb.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.versiondb 的一个方法(会返回当前 MongoDB 服务器的版本):

> db.version
function() {
    return this.serverBuildInfo().version;
}

为了访问 version 集合,必须使用 getCollection 函数:

> db.getCollection("version")
test.version

如果集合名称中包含无效的 JavaScript 属性名称(比如 foo-bar-baz123abc),也可以使用这个函数来访问相应的集合。(注意, 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]

0

评论区