这里是阅读了《MongoDB
权威指南》后做的相关笔记。
一、文档
文档是MongoDB
的核心概念。文档就是键值对的一个有序集合。在JS
中,文档被表示为对象:
{"greeting" : "Hello, world!"}
或
{"greeting" : "Hello, world!", "foo" : 3}
文档的键是字符串。除了少数例外情况,键可以使用任意UTF-8
字符。
- 键不能含有
\0
(空字符)。这个字符用于表示键的结尾。 -
.
和$
具有特殊意义,只能在特定环境下使用。 -
MongoDB
不但区分类型,而且区分大小写。下面两个文旦是不同的:
{"foo" : 3}
{"foo" : "3"}
下面两个文档也是不同的:
{"foo" : 3}
{"Foo" : 3}
- 文档不能有重复的键。而且文档中的键值对是有序的。下面两个文档是不同的:
{"x" : 1, "y" : 2}
{"y" : 2, "x" : 1}
二、集合
集合就是一组文档。
2.1 动态模式
集合是动态的,也就是说,一个集合里的文档可以是各式各样的。如下面的文档可以同时存在于同一个集合中。
{"greeting" : "Hello, world!"}
{"foo" : 5}
2.2 命名
集合使用名称进行标识。集合名可以是满足下列条件的任意UTF-8
字符串。
- 集合名不能是空字符串(
""
)。 - 集合名不能包含
\0
字符串(空字符),这个字符表示集合名的结束。 - 集合名不能以
"system."
开头,这是为系统集合保留的前缀。如,system.users
集合保存着数据库的用户信息,而system.namespaces
集合保存着所有数据库集合的信息。 - 用户创建的集合不能在集合名中包含保留字符
'$'
。因为某些系统生成的集合中包含它。
子集合
组织集合的一种管理是使用"."
分隔不同命名空间的子集合。如,一个具有博客功能的应用可能包含两个集合,分别是blog.posts、blog.authors
。这是为了使组织结构更清晰,这里的blog
集合跟其子集合没有任何关系。
三、 数据库
在MongoDB
中,多个文档年组成集合,而多个集合可以组成数据库。数据库通过名称来标识,这点与集合类似。数据库名可以是满足以下条件的任意UTF-8
字符串。
- 不能是空字符串("")
- 不得含有
/、\、.、"、*、<、>、:、|、?、$
(一个空格)、\0
(空字符)。基本上,只能使用ASCII
中的字母和数字。 - 数据库名区分大小写,即便是在不区分大小写的文件系统中也是如此。简单起见,数据库名应全部小写。
- 数据库名最多
64
字节。
注意:数据库最终会变成文件系统里的文件,而数据库名就是相应的文件名。另外有一些数据库名是保留的,可以直接访问这些有特殊语义的数据库。
admin
从身份验证的角度来讲,这是"root"
数据库。如果将一个用户添加到admin
数据库,这个用户将自动获得所有数据库的权限。再者,一些特定的服务器端命令也只能从admin
数据库运行,如列出所有数据库或关闭服务器。local
这个数据库永远都不可以复制,且一台服务器上的所有本地集合都可以存储在这个数据库中。config
MongoDB
用于分片设置时,分片信息会存储在config
数据库中。
四、MongoDB简介
4.1 shell 中的基本操作
4.1.1 数据库查看
db //表示查看当前使用的是哪个数据库
show dbs //查看所有的数据库
use test //更换数据库为test
4.1.2 创建
说明:这里创建一个名为
post
的局部变量,这是一个JS
对象,用于表示文档。注意,换行的时候不能直接使用回车,应先打//
对回车进行转义。这个对象是一个有效的MongoDB
文档,使用insert
方法可以将其保存到blog
集合中。说明:如上,将文档保存到集合中之后,可以使用
find()
方法查看。同时,可以看到曾输入的完整文档,还有一个额外添加的键"_id"
。其实这就和主键类似。
4.1.3 读取
这里可以使用方法find
和findOne
查询集合里的文档。若只想查看一个文档,可用findOne
。
这两个方法可以接受一个查询文档作为限定条件。这样就可以查询符合一定条件的文档。使用
find
时,shell
会自动显示最多二十个匹配的文档,也可以获得更多文档。
4.1.4 更新
使用update
修改博客文章。其接受(至少)两个参数:第一个是限定条件(用于匹配待更新的文档),第二个是新的文档。现在如果我们要为先前写的文章增加评论功能,就需要增加一个新的键,用于保存评论数组。
说明:这里先修改
post
变量,增加了"comments"
键。然后执行update
操作,其中第一个参数表示限定条件,即匹配标题为"My Blog Post"
的文章。
4.1.5 删除
使用remove
方法可将文档从数据库中永久删除。如果没有使用任何参数,它会将集合内的所有文档全部删除。接受一个作为限定条件的文档作为参数。
五、数据类型
5.1 基本数据类型
在概念上,MongoDB
的文档与JS
中的对象相近,因而可认为它类似于JSON
。JSON
是一种简单的数据表示方式:其规范仅用一段文字就能描述清楚,且仅包含六种数据类型。这其中只有null
、布尔、数字、字符串、数组和对象这几种数据类型,所以JSON
的表达能力有一定的局限。如JSON
没有日期类型。只有一种数字类型,无法区分浮点数和整数,更别说区分32
位和64
位数字了。基于此原因,MongoDB
在保留JSON
基本键值对的基础上,添加了其他一些数据类型。
null
null
用于表示空值或者不存在的字段:
{"x" : null}
- 布尔值
布尔类型有两个值true
和false
:
{"x" : true}
- 数值
shell
默认使用64
位浮点数值。
{"x" : 3.14}
{"x" : 3}
对于整型值,可使用NumberInt
类(表示四字节带符号整数)或NumberLong
类(表示八字节带符号整数),分别举例如下:
{"x" : NumberInt("3")}
{"x" : NumberLong("3")}
- 字符串
UTF-8
字符串都可表示为字符串类型的数据
{"x" : "foobar"}
- 日期
日期被存储为自新纪元来经过的毫秒数,不存储时区:
{"x" : new Date()}
- 正则表达式
查询时,使用正则表达式作为限定条件,语法也与JS
的正则表达式语法相同:
{"x" : /foobar/i}
- 数组
数据列表或数据集可以表示为数组
{"x" : ["a", "b", "c"]}
- 内嵌文档
文档可嵌套其他文档,被嵌套的文档作为父文档的值
{"x" : {"foo" : "bar"}}
- 对象
id
对象id
是一个12
字节的ID
,是文档的唯一标识。
{"x" : ObjectId()}
还有一些不那么常用,但可能有需要的类型,包括下面这些。
二进制数据
二进制数据是一个任意字节的字符串。它不能直接在shell
中使用。如果要将非UTF-8
字符保存到数据库中,二进制数据是唯一的方式。代码
查询和文档中可以包括任意JS
代码
{"x" : function(){/*...*/}}
5.2 日期
在JS
中,Date
类型可以用作MongoDB
的日期类型。创建日期对象时,应使用new Date(...)
,而非Date(...)
。如果将构造函数作为函数进行调用(即不包括new
的方式),返回的是日期的字符串表示,而非日期(Date
)对象。shell
根据本地时区设置显示日期对象。然而,数据库中存储的日期仅为新纪元以来的毫秒数,并未存储对应的时区。(当然,可将时区信息存储为另一个键的值)。
5.3 数组
数组是一组值,它既能作为有序对象(如列表、栈或队列),也能作为无无序对象(如数据集)来操作。在下面的文档中,"things"
这个键的值是一个数组:
{"things" : ["pie", 3.14]}
此例表示,数组可包含不同数据类型的元素(在此,是一个字符串和一个浮点数)。实际上,常规的键值对支持的所有值都可以作为数组的值,数组中甚至可以嵌套数组。
文档中的数组有个奇妙的特性,就是MongoDB
能理解其结构,并知道如何深入数组内部对其内容进行操作。这样就能使用数组内容对数组进行查询和构建索引了。如之前的例子中,可以查询出"things"
数组中包含3.14
这个元素的所有文档。还可以使用原子更新对数组内容进行修改,这在后面讲解。
5.4 内嵌文档
文档可以作为键的值,这样的文档就是内嵌文档。如用一个文档来表示一个人,同时还要保存他的地址,可以将地址信息保存在内嵌的“address”
文档中:
{
"name" : "Tom",
"address" : {
"street" : "123 Park Street",
"city" : "Anytown",
"state" : "NY"
}
}
同数组一样,MongoDB
能够理解内嵌文档的结构, 并能深入其中构建索引,执行查询或更新。在关系型数据库中,这个例子中的文档一般会被拆分成两个表中的两个行("people"
和"address"
各一行)。而MongoDB
中直接将地址文档嵌入到人员文档中。这样做的坏处就是会导致更多的数据重复。假设"address"
是关系数据库中的一个独立的表,我们需要修正地址中的错误。当我们对"people"
和"address"
执行连续操作时,使用这个地址的每个人的信息都会得到更新。但是在MongoDB
中,则需要对每个人的文档分别修正拼写错误。
5.5 _id 和 ObjectId
在MongoDB
中存储的文档必须有一个"_id"
键。这个键的值可以是任何类型的,默认是个ObjectId
对象。在一个集合里面,每个文档都有唯一的"_id"
,确保集合里面每个文档都能被唯一标识。如果有两个集合的话,两个集合可以都有一个"_id"
的值为123
(一个数据库中集合名不能重复),但是每个集合里面只能有一个文档的"_id"
值为123
。
5.5.1 ObjectId
ObjectId
是"_id"
的默认类型。它设计成轻量型的,不同的机器都能用全局唯一的同一种方法方便的生成它。这是MongoDB
不采用其他比较常规的做法(比如自动增加的主键)的主要原因,因为在多个服务器上同步自动增加主键既费时又费力。
ObjectId
使用十二字节的存储空间,是一个由二十四个十六进制数字组成的字符串(每个字节可以存储两个十六进制数字)。如果快速连续创建多个ObjectId
,会发现每次只有最护几位数字有变化。另外,中间的几位数字也会变化(要是在创建的过程中停顿几秒)。这是ObjectId
的创建方式导致的。其十二字节按照如下方式生成:
说明:
ObjectId
的前四个字节是从标准纪元开始的时间戳,单位为秒。这样会带来一些有用的属性。
- 时间戳,与随后的五字节组合起来,提供了秒级别的唯一性。
- 由于时间戳在前,这意味着
ObjectId
大致会按照插入的顺序排列。但是不是绝对的。 - 这四字节也隐含了文档创建的事件。绝大多数驱动程序都会提供一个方法,用于从
ObjectId
获取这些信息。
用户不必担心多服务器时钟同步的问题,因为时间戳的实际值并不重要,只要它总是不停增加就好了(每秒一次)。接下来的三字节是所在主机的唯一标识。通常是机器主机名的散列值。这样就可以确保不同主机生成不同的ObjectId
,不产生冲突。为了确保在同一台机器上并发的多个进程产生的ObjectId
是唯一的,接下来的两字节来自产生ObjectId
的进程的进程标识符(PID
)。
前九字节保证了同一秒钟不同机器不同进程产生的ObjectId
是唯一的。最后三字节是一个自动增加的计数器,确保相同进程同一秒产生的ObjectId
也是不一样的。一秒钟最多允许每个进程拥有2563
个不同的ObjectId
。
5.5.2 自动生成 _id
在创建文档的时候,如果文档没有插入"_id"
键,系统会自动创建一个。
六、使用 MongoDB shell
在之前已经讲过,可以使用命令mongo 127.0.0.1:12345/test
连接相关数据库。而同时我们启动MongoDB
的时候不连接到任何MongoDB
有时也很方便。通过--nodb
参数启动shell
,启动时就不会连接到任何数据库:mongo --nodb
。启动之后,在需要时运行new Mongo(hostname)
命令就可以连接到想要的MongoDB
了:
> conn = new Mongo("some-host:12345")
connection to some-host:12345
> db = conn.getDB("test")
test
6.1 shell 小贴士
可以使用help
命令查看相关帮助,可以通过db.help()
查看数据库级别的帮助,使用db.test.help()
查看集合级别的帮助。如果想知道一个函数是做什么用的,可以直接在shell
输入函数名(函数名后面不要加小括号即可),这样就可以看到相应函数的JS
实现代码。
> db.test.update
6.2 使用shell 执行脚本
我们在MongoDB
中可以直接执行相关的JS
脚本,比如现在给出一个脚本script1.js
(这里只是给出了一个脚本文件,其实可以同时给出多个脚本文件)。其内容如下:
print("Hello World");
执行时,首先启动MongoDB
,然后使用命令
sudo bin/mongo 127.0.0.1:27017/test script1.js
执行。
说明:这里要注意脚本文件所在的目录(使用正确的路径),而且这里还没有连接服务器。这里使用
print()
函数将内容输出到标准输出,而且可以加入--quiet
参数(sudo bin/mongo --quiet 127.0.0.1:27017/test script1.js
)可以不打印MongoDB shell version...
之类的信息。当然,在连接服务器之后也可以执行脚本文件。如下:在脚本中可以访问db
变量,以及其他全局变量。然而,shell
辅助函数(如"use db"
)不可以在文件中使用。这些辅助函数都有对应的JS
函数,如表所示:
辅助函数 | 等价函数 |
---|---|
use foo |
db.getSisterDB("foo") |
show dbs |
db.getMongo().getDBs() |
show collections |
db.getCollectionNames() |
可以使用脚本将变量注入到shell
。例如,下面的脚本对于本书的复制和分片部分内容非常有用。这个脚本定义了一个connectTo()
函数,它连接到指定端口处的一个本地数据库,并且将db
指向这个连接。
defineConnectTo.js
var connectTo = function(port, dbname){
if(!port){
port = 27017;
}
if(!dbname){
dbname = "test";
}
db = connect("localhost:" + port + "/" + dbname);
return db;
};
如果在shell
中加载这个脚本,connectTo
函数就可以使用了。
除了添加辅助函数,还可以使用脚本将通用的任务和管理活动自动化。默认情况下,shell
会在运行shell
时所处的目录中查找脚本(可以使用run("pwd")
命令查看)。如果脚本不在当前目录中,可以为shell
指定一个相对路径或者绝对路径。如可以使用load("/usr/yj-software/my-mongodb/defineConnectTo.js")
来加载defineConnectTo.js
。注意,load
函数无法解析~
符号。也可以在shell中使用run()函数来执行命令行程序。可以在函数参数列表中指定程序所需的参数:
通常来说,这种使用方式的局限性很大,因为输出格式很奇怪,而且不支持管道。