Skip to content

Latest commit

 

History

History
792 lines (623 loc) · 24.3 KB

File metadata and controls

792 lines (623 loc) · 24.3 KB

二、MongoDB 数据建模

数据建模是应用程序构思过程中一个非常重要的过程,因为此步骤将帮助您定义数据库构建的必要需求。此定义正是在数据建模过程中获得的数据理解的结果。

如前所述,无论选择何种数据模型,此过程通常分为两个阶段:一个非常接近用户视图,另一个是将此视图转换为概念模式。在关系数据库建模的场景中,主要的挑战是从这两个阶段构建一个健壮的数据库,目的是保证在应用程序的生命周期中对其进行任何影响的更新。

与关系数据库相比,NoSQL 的一大优势在于,NoSQL 数据库在这一点上更灵活,因为无模式模型在理论上可以在需要修改数据模型时对用户视图造成较小的影响。

尽管 NoSQL 提供了灵活性,但之前知道如何使用数据来建模 NoSQL 数据库是很重要的。最好不要计划要持久化的数据格式,即使在 NoSQL 数据库中也是如此。此外,乍一看,这是数据库管理员非常习惯于关系世界的地方,变得更加不舒服。

关系数据库标准,如 SQL,通过设置规则、规范和标准,给我们带来了安全和稳定的感觉。另一方面,我们将大胆地声明,这种安全性使数据库设计人员远离从中提取要存储的数据的域。

同样的事情也发生在应用程序开发人员身上。他们和数据库管理员之间存在着明显的兴趣分歧,尤其是在数据模型方面。

NoSQL 数据库实际上需要数据库专业人员和应用程序之间的近似值,也需要开发人员和数据库之间的近似值。

因此,即使您可能是一名数据建模师/设计师或数据库管理员,如果从现在开始我们讨论超出您舒适区的主题,也不要害怕。准备好从应用程序开发人员的角度开始使用常用词,并将它们添加到您的词汇表中。本章将介绍 MongoDB 数据模型以及可用于开发和维护该模型的主要概念和结构。

本章将涵盖以下内容:

  • 介绍您的文档和收藏
  • 文件的特点和结构
  • 显示文档的设计和模式

介绍文件和收藏

MongoDB将文档作为数据的基本统一体。MongoDB 中的文档用JavaScript 对象表示法JSON表示)。

集合是组文档。打个比方,集合类似于关系模型中的表,文档是该表中的记录。最后,集合属于 MongoDB 中的数据库。

这些文档以一种称为二进制 JSONBSON)的格式在磁盘上序列化,这是 JSON 文档的二进制表示形式。

文档的一个示例是:

{
   "_id": 123456,
   "firstName": "John",
   "lastName": "Clay",
   "age": 25,
   "address": {
      "streetAddress": "131 GEN. Almério de Moura Street",
      "city": "Rio de Janeiro",
      "state": "RJ",
      "postalCode": "20921060"
   },
   "phoneNumber":[
      {
         "type": "home",
         "number": "+5521 2222-3333"
      },
      {
         "type": "mobile",
         "number": "+5521 9888-7777"
      }
   ]
}

与关系模型不同,在该模型中,您必须声明一个表结构,集合不强制文档的特定结构。集合可能包含结构完全不同的文档。

例如,我们可以在相同的users集合上:

{
   "_id": "123456",
   "username": "johnclay",
   "age": 25,
   "friends":[
      {"username": "joelsant"},
      {"username": "adilsonbat"}
   ],
   "active": true,
   "gender": "male"
}

我们还可以:

{
   "_id": "654321",
   "username": "santymonty",
   "age": 25,
   "active": true,
   "gender": "male",
   "eyeColor": "brown"
}

除此之外,MongoDB 的另一个有趣的特性是,文档不仅仅表示数据。基本上,与 MongoDB 的所有用户交互都是通过文档进行的。除数据记录外,文件还可用于:

  • 定义查询中可以读取、写入和/或更新的数据
  • 定义要更新的字段
  • 创建索引
  • 配置复制
  • 从数据库中查询信息

在深入研究文档的技术细节之前,让我们先了解一下它们的结构。

JSON

JSON是数据开放标准表示的文本格式,非常适合数据流量。为了更深入地探索 JSON 格式,您可以查看ECMA-404 JSON 数据交换标准,其中详细描述了 JSON 格式。

JSON 由两个标准描述:ECMA-404 和 RFC 7159。第一个版本更加关注 JSON 语法和语法,而第二个版本则提供了语义和安全方面的考虑。

顾名思义,JSON 源于 JavaScript 语言。它是作为 web 服务器和浏览器之间对象状态传输的解决方案出现的。尽管是 JavaScript 的一部分,但在几乎所有最流行的编程语言(如 C、Java 和 Python)中都可以找到 JSON 的生成器和读取器。

JSON 格式也被认为是高度友好和可读的。JSON 不依赖于所选择的平台,其规范基于两种数据结构:

  • 一组或一组键/值对
  • 按值排序的列表

所以,为了澄清任何疑问,让我们谈谈对象。对象是键/值对的非有序集合,由以下模式表示:

{
   "key" : "value"
}

对于值排序列表,集合表示如下:

["value1", "value2", "value3"]

在 JSON 规范中,值可以是:

  • " "分隔的字符串
  • 以十进制为基数(以 10 为基数)的数字,带或不带符号。该数字可以有一个小数部分,由一个句点(.分隔),也可以有一个后跟eE的指数部分
  • 布尔值(truefalse
  • Anull
  • 另一个物体
  • 另一个值有序数组

下图显示了 JSON 值结构:

JSON

下面是一个描述一个人的 JSON 代码示例:

{
   "name" : "Han",
   "lastname" : "Solo",
   "position" : "Captain of the Millenium Falcon",
   "species" : "human",
   "gender":"male",
   "height" : 1.8
}

BSON

BSON表示二进制 JSON,也就是说,表示 JSON 文档的二进制编码序列化。

如果您想了解更多关于 BSON 的知识,我建议您看看上的 BSON 规范http://bsonspec.org/

如果我们将 BSON 与其他二进制格式进行比较,BSON 的优势在于它是一种允许您更灵活的模型。另外,它的一个特点是它的轻量级——这一特性对于 Web 上的数据传输非常重要。

对于大多数基于 C 的编程语言,BSON 格式设计为易于导航,并以非常高效的方式进行编码和解码。这就是为什么选择 BSON 作为 MongoDB 磁盘持久性的数据格式的原因。

BSON 中的数据表示类型有:

  • 字符串 UTF-8(string
  • 整数 32 位(int32
  • 整数 64 位(int64
  • 浮点(double
  • 文件(document
  • 数组(document
  • 二进制数据(binary
  • 布尔值为 false(\x00或字节 0000)
  • 布尔值为真(\x01或字节 0000 0001)
  • UTC 日期时间(int64)-int64 是自 Unix 纪元以来的 UTC 毫秒
  • Timestamp(int64)-这是 MongoDB 复制和分片使用的特殊内部类型;前 4 个字节是增量,后 4 个字节是时间戳
  • 空值()
  • 正则表达式(cstring
  • JavaScript 代码(string
  • 带作用域的 JavaScript 代码(code_w_s
  • Min key()-比较所有其他可能 BSON 元素值的较低值的特殊类型
  • Max key()-比较高于所有其他可能 BSON 元素值的值的特殊类型
  • ObjectId(byte*12)

文件的特点

在详细说明我们必须如何对文档建模之前,我们需要更好地了解它的一些特性。这些特征可以决定文档必须如何建模。

文件大小

我们必须记住,BSON 文档的最大长度是 16MB。根据 BSON 规范,此长度非常适合通过 Web 进行数据传输,并避免过度使用 RAM。但这只是一个建议。现在,使用 GridFS 可以使文档长度超过 16MB。

GridFS 允许我们在 MongoDB 中存储大于 BSON 最大大小的文档,方法是将其划分为多个部分或区块。每个区块都是一个大小为 255k 的新文档。

文档中字段的名称和值

关于文档中字段的名称和值,您必须了解一些事情。首先,文档中任何字段的名称都是字符串。像往常一样,我们对字段名有一些限制。他们是:

  • _id字段是为主键保留的
  • 无法使用字符$启动名称
  • 名称不能有空字符,或(.

此外,具有索引字段的文档必须遵守索引字段的大小限制。这些值不能超过 1024 字节的最大大小。

文档主键

如前一节所示,字段是为主键保留的。默认情况下,此字段必须是文档中的第一个字段,即使在插入过程中,它不是要插入的第一个字段。在这些情况下,MongoDB 将其移动到第一个位置。此外,根据定义,将在该字段中创建唯一索引。

_id字段可以有任何 BSON 类型的值,数组除外。此外,如果创建的文档没有显示_id字段,MongoDB 将自动创建 ObjectId 类型的_id字段。然而,这不是唯一的选择。您可以使用任何要标识文档的值,只要它是唯一的。还有另一种选择,即基于支持集合或乐观循环生成自动增量值。

支持集合

在这个方法中,我们使用一个单独的集合来保存序列中最后使用的值。为了增加序列,首先我们应该查询最后使用的值。之后,我们可以使用操作符$inc来增加值。

有一个名为system.js的集合,它可以保存 JavaScript 代码以便重用。小心不要将应用程序逻辑包含在此集合中。

让我们看一下此方法的一个示例:

db.counters.insert(
 {
 _id: "userid",
 seq: 0
 }
)

function getNextSequence(name) {
 var ret = db.counters.findAndModify(
 {
 query: { _id: name },
 update: { $inc: { seq: 1 } },
 new: true
 }
 );
 return ret.seq;
}

db.users.insert(
 {
 _id: getNextSequence("userid"),
 name: "Sarah C."
 }
)

乐观循环

乐观循环生成的字段是通过每次迭代递增来完成的,之后,尝试将其插入新文档中:

function insertDocument(doc, targetCollection) {
    while (1) {
        var cursor = targetCollection.find( {}, { _id: 1 } ).sort( { _id: -1 } ).limit(1);
        var seq = cursor.hasNext() ? cursor.next()._id + 1 : 1;
        doc._id = seq;
        var results = targetCollection.insert(doc);
        if( results.hasWriteError() ) {
            if( results.writeError.code == 11000 /* dup key */ )
                continue;
            else
                print( "unexpected error inserting data: " + tojson( results ) );
        }
        break;
    }
}

在此函数中,迭代执行以下操作:

  1. targetCollection中搜索_id的最大值。
  2. 确定_id的下一个值。
  3. 设置要插入的文档上的值。
  4. 插入文档。
  5. 如果由于重复的_id字段而导致错误,则循环将重复自身,否则迭代将结束。

这里演示的要点是理解此工具可以提供的所有可能性和方法的基础。但是,尽管我们可以为 MongoDB 使用自动递增字段,但我们必须避免使用它们,因为该工具无法扩展到巨大的数据量。

设计文档

在这一点上,我相信您一定在问自己:如果文档的基本结构是 JSON(如此简单和文本化的东西),那么创建 NoSQL 数据库有什么可能如此复杂?

让我看看!首先,是的!你是对的。NoSQL 数据库可以非常简单。但是,这并不意味着它们的结构没有关系数据库复杂。不过,情况会有所不同!

如前所述,集合不会强制您事先定义文档的结构。但这肯定是你必须在某个时候做出的决定。这个决定将影响重要的方面,特别是在有关查询性能的问题上。

到目前为止,您可能还会问自己应用程序如何表示文档之间的关系。如果你到现在还没有想到这一点,那不是你的错。我们习惯于思考关系世界,比如想知道学生和他们的班级之间的关系,或者产品和订单之间的关系。

MongoDB 也有自己的方式来表达这种关系。事实上,有两种方法:

  • 嵌入文档
  • 工具书类

处理嵌入式文档

通过子文档的使用,我们可以构建更加复杂和优化的数据结构。因此,当我们对文档建模时,我们可以选择将相关数据嵌入到一个文档中。

在一个文档中嵌入数据的决定通常与获得更好的读取性能的意图有关,因为通过一个查询,我们可以完全检索到我们需要的信息。

请参见以下示例:

{
   id: 1,
   title: "MongoDB Data Modeling Blog post",
   body: "MongoDB Data Modeling....",
   author: "Wilson da Rocha França",
   date: ISODate("2014-11-19"),
   comments: [
      {
         name: "Mike",
         email : "mike@mike.com",
         comment: "Mike comment...."
      },
      {
         name: "Tom",
         email : "tom@tom.com",
         comment: "Tom comment...."
      },
      {
         name: "Yuri",
         email : "yuri@yuri.com",
         comment: "Yuri comment...."
      }
   ],
   tags: ["mongodb", "modeling", "nosql"]
}

我们可以推断,此文档代表一篇博客文章。这种文档的优点是,只需一次查询,我们就可以向用户提供所需的所有数据。这同样适用于更新:只需一个查询,我们就可以修改此文档的内容。然而,当我们决定嵌入数据时,我们必须确保文档不超过 BSON 16MB 的大小限制。

在 MongoDB 中嵌入数据时没有规则,但总的来说,我们应该遵守:

  • 文件之间是否存在一对一的关系。
  • 文档之间是否存在一对多关系,以及关系的“多”部分是否与“一”部分密切相关。这意味着,例如,每次我们呈现“一”部分时,我们也会呈现关系的“多”部分。

如果我们的模型 To.T0R 适合前面的一个场景,我们应该考虑使用嵌入式文档。

使用参考资料

规范化是帮助建立关系数据模型的基本过程。为了最小化冗余,在这个过程中,我们将较大的表划分为较小的表,并定义它们之间的关系。我们可以说,在 MongoDB 中创建引用是我们必须“规范化”模型的方式。本参考文件将描述文件之间的关系。

您可能会对我们为什么要考虑非关系世界中的关系感到困惑,尽管这并不意味着 NoSQL 数据库中不存在关系。我们将经常使用关系建模的概念来解决常见问题。如前所述,为了消除冗余,文档可以相互引用。

但是等等!从现在起,您应该知道一件非常重要的事情:MongoDB 不支持连接。这意味着,即使引用了另一个文档,也必须执行至少两个查询才能获得所需的完整信息。

请参见以下示例:

{
   _id: 1,
   name : "Product 1",
   description: "Product 1 description",
   price: "$10,00",
   supplier : { 
      name: "Supplier 1", 
      address: "St.1", 
      telephone: "+552199999999" 
   }
}

{
   _id: 2,
   name : "Product 2",
   description: "Product 2 description",
   price: "$10,00",
   supplier : { 
      name: "Supplier 1", 
      address: "St.1", 
      telephone: "+552199999999" 
   }
}

{
   _id: 3,
   name : "Product 3",
   description: "Product 3 description",
   price: "$10,00",
   supplier : { 
      name: "Supplier 1", 
      address: "St.1", 
      telephone: "+552199999999" 
   }
}

在前面的示例中,我们有products集合中的文档。我们可以看到,在这三种产品的实例中,我们对供应商密钥具有相同的值。与重复数据不同,我们可以有两个集合:productssuppliers,如下面的示例所示:

suppliers

{
   _id: 1
   name: "Supplier 1", 
   address: "St.1", 
   telephone: "+552199999999",
   products: [1, 2, 3]
}

products 

{
   _id: 1,
   name : "Product 1",
   description: "Product 1 description",
   price: "$10,00"
}

{
   _id: 2,
   name : "Product 2",
   description: "Product 2 description",
   price: "$10,00"
}

{
   _id: 3,
   name : "Product 3",
   description: "Product 3 description",
   price: "$10,00"
}

在这种特殊情况下,由于供应商提供了一些产品,因此选择基于供应商的参考产品是很好的。但是,如果情况正好相反,则更好的方法是:

suppliers

{
   _id: 1
   name: "Supplier 1", 
   address: "St.1", 
   telephone: "+552199999999"
}

products 

{
   _id: 1,
   name : "Product 1",
   description: "Product 1 description",
   price: "$10,00",
   supplier: 1
}

{
   _id: 2,
   name : "Product 2",
   description: "Product 2 description",
   price: "$10,00",
   supplier: 1
}

{
   _id: 3,
   name : "Product 3",
   description: "Product 3 description",
   price: "$10,00",
   supplier: 1
}

使用 MongoDB 的引用没有规则,但总体而言,我们应该遵守:

  • 是否在嵌入数据时多次复制相同的信息(这表明读取性能较差)
  • 我们是否需要表示多对多关系
  • 我们的模型是否是层次结构

如果我们的模型适合于前面的一个场景,我们应该考虑引用的使用。

原子性

另一个在设计文档时会影响我们决策的重要概念是原子性。在 MongoDB 中,操作是文档级别的原子操作。这意味着我们可以一次修改一个文档。即使有一个操作在集合中的多个文档中工作,此操作也会一次在一个文档中执行。

因此,当我们决定用嵌入数据对文档建模时,我们只需编写操作,因为我们需要的所有数据都在同一个文档中。这与我们选择引用数据时发生的情况相反,在引用数据时,我们需要许多非原子的写操作。

常见文档模式

现在我们已经了解了设计文档的方法,让我们举一些实际问题的例子,比如如何编写更好地描述实体之间关系的数据模型。

本节将向您介绍说明何时嵌入或何时引用文档的模式。到目前为止,我们一直将以下因素视为决定因素:

  • 一致性是否是优先事项
  • 是否读取是优先事项
  • 是否写是优先考虑的
  • 我们将进行哪些更新查询
  • 文档增长

一对一

一对一关系比其他关系简单。大多数情况下,我们会将这种关系映射到嵌入式文档,特别是当它是“包含”关系时。

下面的示例显示了客户的文档。在第一种情况下,customerDetails文档中存在引用;在第二个示例中,我们看到一个带有嵌入数据的引用:

  • 参考资料:

    customer
    { 
       "_id": 5478329cb9617c750245893b
       "username" : "John Clay",
       "email": "johnclay@crgv.com",
       "password": "bf383e8469e98b44895d61b821748ae1"
    }
    customerDetails
    {
       "customer_id": "5478329cb9617c750245893b",
       "firstName": "John",
       "lastName": "Clay",
       "gender": "male",
       "age": 25
    }
  • 带嵌入式数据:

    customer
    { 
       _id: 1
       "username" : "John Clay",
       "email": "johnclay@crgv.com",
       "password": "bf383e8469e98b44895d61b821748ae1"
       "details": {
     "firstName": "John",
     "lastName": "Clay",
     "gender": "male",
     "age": 25
     }
    }

使用嵌入文档表示关系的优点是,在查询客户时,客户详细信息数据始终可用。因此,我们可以说,客户的详细信息本身没有意义,只能与客户数据一起使用。

一对多

一对多关系比一对一关系更复杂。为了决定何时嵌入或引用,我们必须考虑关系的“多方”。如果多个面应该与其父面一起显示,那么我们应该选择嵌入数据;否则,我们可以使用父母的参考资料。

我们来看一个customer和客户地址的例子:

customer
{ 
   _id: 1
   "username" : "John Clay",
   "email": "johnclay@crgv.com",
   "password": "bf383e8469e98b44895d61b821748ae1"
   "details": {
      "firstName": "John",
      "lastName": "Clay",
      "gender": "male",
      "age": 25
   }
}

address
{
   _id: 1,
   "street": "Address 1, 111",
   "city": "City One",
   "state": "State One",
   "type": "billing",
   "customer_id": 1
}
{
   _id: 2,
   "street": "Address 2, 222",
   "city": "City Two",
   "state": "State Two",
   "type": "shipping",
   "customer_id": 1
}
{
   _id: 3,
   "street": "Address 3, 333",
   "city": "City Three",
   "state": "State Three",
   "type": "shipping",
   "customer_id": 1
}

如果每次要显示客户的地址,还需要显示客户的姓名,建议嵌入文档:

customer
{ 
   _id: 1
   "username" : "John Clay",
   "email": "johnclay@crgv.com",
   "password": "bf383e8469e98b44895d61b821748ae1"
   "details": {
      "firstName": "John",
      "lastName": "Clay",
      "gender": "male",
      "age": 25
   }
   "billingAddress": [{
      "street": "Address 1, 111",
      "city": "City One",
      "state": "State One",
      "type": "billing",
   }],

   "shippingAddress": [{
      "street": "Address 2, 222",
      "city": "City Two",
      "state": "State Two",
      "type": "shipping"
   },
   {
      "street": "Address 3, 333",
      "city": "City Three",
      "state": "State Three",
      "type": "shipping"
   }]
}

多对多

一个多对多关系不是一件小事,即使在一个关系世界中也是如此。在关系世界中,这种关系通常表示为联接表,而在非关系世界中,它可以用许多不同的方式表示。

在下面的代码中,我们将看到一个经典的usergroup关系示例:

user

{
   _id: "5477fdea8ed5881af6541bf1",
   "username": "user_1",
   "password" : "3f49044c1469c6990a665f46ec6c0a41"
}

{
   _id: "54781c7708917e552d794c59",
   "username": "user_2",
   "password" : "15e1576abc700ddfd9438e6ad1c86100"
}

group

{
   _id: "54781cae13a6c93f67bdcc0a",
   "name": "group_1"
}

{
   _id: "54781d4378573ed5c2ce6100",
   "name": "group_2"
}

现在让我们将关系存储在User文档中:

user

{
   _id: "5477fdea8ed5881af6541bf1",
   "username": "user_1",
   "password" : "3f49044c1469c6990a665f46ec6c0a41",
   "groups": [
      {
         _id: "54781cae13a6c93f67bdcc0a",
         "name": "group_1"
      },
      {
         _id: "54781d4378573ed5c2ce6100",
         "name": "group_2"
      }

   ]
}

{
   _id: "54781c7708917e552d794c59",
   "username": "user_2",
   "password" : "15e1576abc700ddfd9438e6ad1c86100",
   "groups": [
      {
         _id: "54781d4378573ed5c2ce6100",
         "name": "group_2"
      }

   ]
}

group
{
   _id: "54781cae13a6c93f67bdcc0a",
   "name": "group_1"
}

{
   _id: "54781d4378573ed5c2ce6100",
   "name": "group_2"
}

或者我们可以将关系存储在group文档中:

user
{
   _id: "5477fdea8ed5881af6541bf1",
   "username": "user_1",
   "password" : "3f49044c1469c6990a665f46ec6c0a41"
}
{
   _id: "54781c7708917e552d794c59",
   "username": "user_2",
   "password" : "15e1576abc700ddfd9438e6ad1c86100"
}
group
{
   _id: "54781cae13a6c93f67bdcc0a",
   "name": "group_1",
   "users": [
      {
         _id: "54781c7708917e552d794c59",
         "username": "user_2",
         "password" : "15e1576abc700ddfd9438e6ad1c86100"
      }

   ]
}
{
   _id: "54781d4378573ed5c2ce6100",
   "name": "group_2",
   "users": [
      {
         _id: "5477fdea8ed5881af6541bf1",
         "username": "user_1",
         "password" :  "3f49044c1469c6990a665f46ec6c0a41"
      },
      {
         _id: "54781c7708917e552d794c59",
         "username": "user_2",
         "password" :  "15e1576abc700ddfd9438e6ad1c86100"
      }

   ]
}

并且,最后,让我们在两个文档中存储关系:

user
{
   _id: "5477fdea8ed5881af6541bf1",
   "username": "user_1",
   "password" : "3f49044c1469c6990a665f46ec6c0a41",
   "groups": ["54781cae13a6c93f67bdcc0a", "54781d4378573ed5c2ce6100"]
}
{
   _id: "54781c7708917e552d794c59",
   "username": "user_2",
   "password" : "15e1576abc700ddfd9438e6ad1c86100",
   "groups": ["54781d4378573ed5c2ce6100"]
}
group
{
   _id: "54781cae13a6c93f67bdcc0a",
   "name": "group_1",
   "users": ["5477fdea8ed5881af6541bf1"]
}
{
   _id: "54781d4378573ed5c2ce6100",
   "name": "group_2",
   "users": ["5477fdea8ed5881af6541bf1", "54781c7708917e552d794c59"]
}

总结

在本章中,您了解了如何在 MongoDB 中构建文档,检查了它们的特征,并了解了它们是如何组织到集合中的。

现在,您了解了了解应用程序的领域对于设计尽可能好的模型是多么重要,并且看到了一些可以帮助您决定如何设计文档的模式。

在下一章中,我们将了解如何查询这些集合并修改其中存储的数据。