MongoDB权威指南(第2版)
上QQ阅读APP看书,第一时间看更新

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字节按照如下方式生成:

ObjectId的前4个字节是从标准纪元开始的时间戳,单位为秒。这会带来一些有用的属性。

· 时间戳,与随后的5字节(稍后介绍)组合起来,提供了秒级别的唯一性。

· 由于时间戳在前,这意味着ObjectId大致会按照插入的顺序排列。这对于某些方面很有用,比如可以将其作为索引提高效率,但是这个是没有保证的,仅仅是“大致”。

· 这4字节也隐含了文档创建的时间。绝大多数驱动程序都会提供一个方法,用于从ObjectId获取这些信息。

因为使用的是当前时间,很多用户担心要对服务器进行时钟同步。虽然在某些情况下,在服务器间进行时间同步确实是个好主意(参见23.6.1节),但是这里其实没有必要,因为时间戳的实际值并不重要,只要它总是不停增加就好了(每秒一次)。

接下来的3字节是所在主机的唯一标识符。通常是机器主机名的散列值(hash)。这样就可以确保不同主机生成不同的ObjectId,不产生冲突。

为了确保在同一台机器上并发的多个进程产生的ObjectId是唯一的,接下来的两字节来自产生ObjectId的进程的进程标识符(PID)。

前9字节保证了同一秒钟不同机器不同进程产生的ObjectId是唯一的。最后3字节是一个自动增加的计数器,确保相同进程同一秒产生的ObjectId也是不一样的。一秒钟最多允许每个进程拥有2563(16777216)个不同的ObjectId。

2.自动生成_id

前面讲到,如果插入文档时没有"_id"键,系统会自动帮你创建一个。可以由MongoDB服务器来做这件事,但通常会在客户端由驱动程序完成。这一做法非常好地体现了MongoDB的哲学:能交给客户端驱动程序来做的事情就不要交给服务器来做。这种理念背后的原因是,即便是像MongoDB这样扩展性非常好的数据库,扩展应用层也要比扩展数据库层容易得多。将工作交由客户端来处理,就减轻了数据库扩展的负担。