一、数据模型介绍

MongoDB中的数据有着灵活的架构。与SQL数据库不同,因为SQL数据库必须先定义表结构,然后才能向其中插入数据,而MongoDB的集合不强制任何文档结构。这个灵活性方便了文档与实体或者对象之间的映射。每个文档可以匹配所表示实体的数据域,哪怕这个数据后面会发生变化。当然实际应用中,最好还是让集合中的文档有着类似的结构。

数据模型最富有挑战的意义是在于能平衡应用需要与数据库引擎性能以及数据获取模式。当设计数据模型时,总是会考虑应用程序对数据的使用(如查询、更新和数据处理),以及数据本身的继承结构。

文档结构

为MongoDB应用程序设计数据模型的关键点是解析文档的结构和应用程序如何表示数据之间的关系。应用程序表示数据关系可以有两种方式:引用和嵌入文档。

引用

引用通过包含链接或从一个文档到另一个文档的引用来存储数据关系。应用程序可以通过解析这些引用来访问有关数据。一般地我们称之为规范化数据模型。

嵌入数据

嵌入文档通过把数据存储到一个独立文档结构中来获取数据之间的关系。MongoDB允许将一个文档结构嵌入到另一个文档的字段或者数组中。这些去规范化数据模型允许应用程序在一个独立的数据库操作中获取和操作有关数据。

写操作的原子性

MongoDB的写操作在文档级别是原子性的,没有单个写操作对超过一个文档或者超过一个集合是原子性的。带有嵌入数据的去规范化数据模型将一个可表现实体的所有有关数据合并到单个文档中。这有益于原子写操作,因为单个写操作可以对一个实体实现插入和更新。规范数据意味着把数据切分到不同的集合,这需要多个写操作,而这些写操作虽然自己本身是原子性的,但是合并起来看则不具有原子性。

文档增长

有些更新,如将元素压入数组或者添加新字段,则会增加文档大小。

对MMAPv1存储引擎而言,如果文档大小超过为这个文档分配的空间,MongoDB会在磁盘上迁移此文档。当使用MMAPv1存储引擎时,文档增长的考虑会影响我们规范化数据还是去规范化数据。

数据使用和性能

设计数据模型时,考虑一下应用程序如何使用数据库。例如,如果应用程序仅使用最近插入的文档,那可以考虑使用Capped Collections。如果程序主要是对集合的读操作,那么对集合添加索引可以提高性能。

二、文档验证

MongoDB在更新和插入操作期间可以验证文档。验证规则使用validator选项在每个集合上指定,这个validator选项用一个文档具现化验证规则和表达式。这些表达式的指定可以使用任何查询操作符,除了$geoNear, $near, $nearSphere, $text和$where。

如要对一个集合添加文档验证,使用带validator选项的collMod命令。你可以在创建新集合时就指定文档验证规则,即,使用带有validator选项的db.createCollection()方法,如下所示:

db.createCollection( "contacts",
   { validator: { $or:
      [
         { phone: { $type: "string" } },
         { email: { $regex: /@mongodb\.com$/ } },
         { status: { $in: [ "Unknown", "Incomplete" ] } }
      ]
   }
} )

MongoDB提供了validationLevel选项,这个选项决定了MongoDB在更新文档时应用验证规则的严格程度。还有一个是validationAction选项,这决定了MongoDB是否引发错误并拒绝违背验证规则的文档,还是给出警告并把违背验证规则消息写入日志同时接受无效的文档。

行为

验证在更新和插入时验证。当你添加验证规则到一个集合时,已经存在的文档不会被验证是否符合验证规则,除非这些文档被修改。

存在的文档

可以使用validationLevel选项来控制MongoDB如何处理已存在的文档,即是否验证之前已存在的规则。

默认情况下,validationLevel值为strict,MongoDB应用验证规则到所有的插入和更新操作。设置validationLevel为moderate,则应用验证规则到插入操作和对已存在且满足验证标准的文档的更新操作。在moderate级别下,对不满足验证标准的已存在文档,MongoDB则不检测这些文档的有效性。

例子:

考虑以下contacts集合中的文档:

{
   "_id": "125876"
   "name": "Anne",
   "phone": "+1 555 123 456",
   "city": "London",
   "status": "Complete"
},
{
   "_id": "860000",
   "name": "Ivan",
   "city": "Vancouver"
}

用以下命令对contacts集合添加一个验证器

db.runCommand( {
   collMod: "contacts",
   validator: { $or: [ { phone: { $exists: true } }, { email: { $exists: true } } ] },
   validationLevel: "moderate"
} )

现在contacts集合有一个moderate级别的验证器。如果试图更新_id为125876的文档,MongoDB将应用验证规则,因为这个已存在文档匹配验证条件。相反地,MongoDB不会应用验证规则到_id为860000的文档更新操作上,因为这个文档原先不满足验证规则。

如果想完全禁用验证规则,可以设置validationLevel为off。

接受或拒绝无效文档

validationAction选项决定了MongoDB如何处理违反验证规则的文档。

默认地,validationAction为error,这时MongoDB拒绝任何违反验证条件的插入或更新操作。当validationAction值设置为warn,MongoDB记录任何违反验证的信息到日志但是允许插入或者更新的操作处理。

例如,下例创建一个contacts集合,这个集合带有一个验证器,这个验证器指定插入和更新文档需要满足以下三个条件中的至少一个条件:

  • phone字段是string类型
  • email字符满足正则表达式
  • status字段要么是Unknown,要么是Incomplete
db.createCollection( "contacts",
   {
      validator: { $or:
         [
            { phone: { $type: "string" } },
            { email: { $regex: /@mongodb\.com$/ } },
            { status: { $in: [ "Unknown", "Incomplete" ] } }
         ],
       validationAction: "warn"
      }
   }
)

有了验证器之后,以下插入操作不满足验证规则,但是因为validationAction值为warn,故写操作记录此信息到日志。

db.contacts.insert( { name: "Amanda", status: "Updated" } )

日志信息包含了集合的全命名空间和不满足验证规则的文档,以及操作时间

2015-10-15T11:20:44.260-0400 W STORAGE  [conn3] Document would fail validation collection: example.contacts doc: { _id: ObjectId('561fc44c067a5d85b96274e4'), name: "Amanda", status: "Updated" }

限制

我们不能为admin,local和config数据库指定验证器,也不能对system.*集合添加验证器。

绕过文档验证

用户可以使用bypassDocumentValidation选项绕过文档验证,参考Document Validation可以获得支持bypassDocumentValidation选项的命令的列表。

如果部署已经允许访问控制,为了绕过文档验证,已认证用户必须需要bypassDocumentValidation动作。内建的dbAdmin和restore角色提供了这个动作。

三、数据模型设计

 有效的数据模型可以支持程序的需求。关键考虑点便是文档结构采用嵌入式还是引用式。

嵌入数据模型

上面已经介绍过,即去规范化数据模型

引用数据模型

上面已经介绍过,即规范化数据模型

嵌入文档的一对一模型

考虑以下patron与address之间的关系。这个例子表明了如果需要查看处于其他上下文中的数据实体,那么嵌入式比引用式数据模型具有优势。在这个patron和address数据之间的一对一关系中,address属于patron

在规范化数据模型中,address文档包含了一个队patron文档的引用。

{
   _id: "joe",
   name: "Joe Bookreader"
}

{
   patron_id: "joe",
   street: "123 Fake Street",
   city: "Faketon",
   state: "MA",
   zip: "12345"
}

如果address数据需要频繁获取名字信息,那在引用式数据模型中,程序需要进行多个查询来解析这个引用。更好的数据模型会把address数据嵌入到patron数据中,如下

{
   _id: "joe",
   name: "Joe Bookreader",
   address: {
              street: "123 Fake Street",
              city: "Faketon",
              state: "MA",
              zip: "12345"
            }
}

这样,程序在一次查询中就能获取全部信息。

嵌入文档的一对多关系模型

以下例子中,patron与address数据之间存在一对多关系,即patron有多个address实体。

在规范化数据模型中,address文档包含了对patron文档的引用,如下

{
   _id: "joe",
   name: "Joe Bookreader"
}

{
   patron_id: "joe",
   street: "123 Fake Street",
   city: "Faketon",
   state: "MA",
   zip: "12345"
}

{
   patron_id: "joe",
   street: "1 Some Other Street",
   city: "Boston",
   state: "MA",
   zip: "12345"
}

如果程序频繁获取address数据中的名称信息,那么需要多次查询以解析引用。一个较优的方案是嵌入address数据到patron数据中,如下

{
   _id: "joe",
   name: "Joe Bookreader",
   addresses: [
                {
                  street: "123 Fake Street",
                  city: "Faketon",
                  state: "MA",
                  zip: "12345"
                },
                {
                  street: "1 Some Other Street",
                  city: "Boston",
                  state: "MA",
                  zip: "12345"
                }
              ]
 }

这样通过一次查询,程序就能获取全部的patron信息。

文档引用的一对多关系模型

考虑以下出版社与书之间关系映射。这个例子说明了引用比嵌入具有的优势,避免了出版社信息的重复。

嵌入式数据模型如下

{
   title: "MongoDB: The Definitive Guide",
   author: [ "Kristina Chodorow", "Mike Dirolf" ],
   published_date: ISODate("2010-09-24"),
   pages: 216,
   language: "English",
   publisher: {
              name: "O'Reilly Media",
              founded: 1980,
              location: "CA"
            }
}

{
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English",
   publisher: {
              name: "O'Reilly Media",
              founded: 1980,
              location: "CA"
            }
}

可见,这种数据模型导致出版社信息的重复,使用引用数据模型使出版社信息在一个独立的集合中则可以解决冗余问题。

当使用引用数据模型时,关系的增长决定将引用存储到何处。如果每个出版社的书数量增长缓慢,那将书的引用存储到出版社文档中可能会是不错的主意。否则如果每个出版社的书数量没有界限,那这个数据模型会导致可变的,增长数组,如下所示,

{
   name: "O'Reilly Media",
   founded: 1980,
   location: "CA",
   books: [12346789, 234567890, ...]  // 随着书的数量增长
}

{
    _id: 123456789,
    title: "MongoDB: The Definitive Guide",
    author: [ "Kristina Chodorow", "Mike Dirolf" ],
    published_date: ISODate("2010-09-24"),
    pages: 216,
    language: "English"
}

{
   _id: 234567890,
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English"
}

为了避免文档中可变的增长数组,存储出版社信息到书文档中,如下

{
   _id: "oreilly",
   name: "O'Reilly Media",
   founded: 1980,
   location: "CA"
}

{
   _id: 123456789,
   title: "MongoDB: The Definitive Guide",
   author: [ "Kristina Chodorow", "Mike Dirolf" ],
   published_date: ISODate("2010-09-24"),
   pages: 216,
   language: "English",
   publisher_id: "oreilly"
}

{
   _id: 234567890,
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English",
   publisher_id: "oreilly"
}