赞
踩
一、数据建模的介绍
数据建模中的关键挑战是平衡应用程序的需求、数据库引擎的性能特征和数据检索模式。在设计数据模型时,一定要考虑数据的应用使用(即数据的查询、更新和处理)以及数据本身的固有结构。
1.灵活的模式
SQL数据库在插入数据之前必须确定和声明表的模式,而MongoDB的集合在默认情况下不要求其文档具有相同的模式。那就是:
这种灵活性有助于将文档映射到实体或对象。每个文档都可以匹配表示的实体的数据字段,即使该文档与集合中的其他文档有很大的差异。
然而,在实践中,集合中的文档共享类似的结构,您可以在更新和插入操作期间为集合实施文档验证规则。有关详细信息,请参阅模式验证。
2. 文档结构
为MongoDB应用程序设计数据模型的关键决策是围绕文档的结构和应用程序如何表示数据之间的关系。MongoDB允许在单个文档中嵌入相关数据。
2.1 嵌入式数据
嵌入式文档通过在单个文档结构中存储相关数据来捕获数据之间的关系。MongoDB文档使得在文档的字段或数组中嵌入文档结构成为可能。这些非规范化的数据模型允许应用程序在单个数据库操作中检索和操作相关数据。
对于MongoDB中的许多用例,非规范化数据模型是最优的。
有关嵌入文档的优缺点,请参阅嵌入数据模型。
2.2 参考文献
引用通过包含从一个文档到另一个文档的链接或引用来存储数据之间的关系。应用程序可以解析这些引用来访问相关数据。一般来说,这些都是规范化的数据模型。
3. 写操作的原子性
3.1 单文档原子性
在MongoDB中,写操作是单个文档级别上的原子操作,即使该操作修改了单个文档中的多个嵌入文档。
具有嵌入式数据的非规范化数据模型将所有相关数据组合在一个文档中,而不是跨多个文档和集合进行规范化。这个数据模型简化了原子操作。
当单个写操作(例如db.collection.updateMany())修改多个文档时,对每个文档的修改是原子性的,但整个操作不是原子性的。
在执行多文档写操作时,无论是通过单个写操作还是多个写操作,其他操作可能会交错进行。
对于需要对多个文档进行原子性读写的情况(在单个或多个集合中),MongoDB支持多文档事务:
3.2 多文档事务
对于需要对多个文档进行原子性读写的情况(在单个或多个集合中),MongoDB支持多文档事务:
有关MongoDB事务的详细信息,请参阅事务页面。
重要的:
在大多数情况下,与单个文档写入相比,多文档事务会带来更大的性能成本,而且多文档事务的可用性不应该代替有效的模式设计。对于许多场景,非规范化数据模型(嵌入文档和数组)对于您的数据和用例仍然是最优的。也就是说,对于许多场景,适当地对数据建模将最小化对多文档事务的需求。
有关其他事务使用注意事项(如运行时限制和oplog大小限制),请参见生产
4. 数据使用和性能
在设计数据模型时,考虑应用程序将如何使用数据库。例如,如果您的应用程序只使用最近插入的文档,那么可以考虑使用有上限的集合。或者,如果您的应用程序需要的主要是对集合的读操作,那么添加索引来支持通用查询可以提高性能。
有关这些和其他影响数据模型设计的操作注意事项的更多信息,请参见操作因素和数据模型。
二、模式验证
新版本3.2。
MongoDB提供了在更新和插入期间执行模式验证的功能。
1. 指定验证规则
验证规则基于每个集合。
要在创建新集合时指定验证规则,可以使用带有验证器选项的db.createCollection()。
若要将文档验证添加到现有集合,请使用带有验证器选项的collMod命令。
MongoDB还提供了以下相关选项:
2. JSON模式
新版本3.6。
从3.6版开始,MongoDB支持JSON模式验证。要指定JSON模式验证,请在验证器表达式中使用$jsonSchema操作符。
请注意:
建议使用JSON模式执行模式验证。
例如,下面的示例使用JSON模式指定验证规则:
- db.createCollection("students", {
- validator: {
- $jsonSchema: {
- bsonType: "object",
- required: [ "name", "year", "major", "address" ],
- properties: {
- name: {
- bsonType: "string",
- description: "must be a string and is required"
- },
- year: {
- bsonType: "int",
- minimum: 2017,
- maximum: 3017,
- description: "must be an integer in [ 2017, 3017 ] and is required"
- },
- major: {
- enum: [ "Math", "English", "Computer Science", "History", null ],
- description: "can only be one of the enum values and is required"
- },
- gpa: {
- bsonType: [ "double" ],
- description: "must be a double if the field exists"
- },
- address: {
- bsonType: "object",
- required: [ "city" ],
- properties: {
- street: {
- bsonType: "string",
- description: "must be a string if the field exists"
- },
- city: {
- bsonType: "string",
- "description": "must be a string and is required"
- }
- }
- }
- }
- }
- }
- })
3. 其他查询表达式
除了使用$jsonSchema查询操作符的JSON模式验证外,MongoDB还支持使用其他查询操作符进行验证,除了$near、$near sphere、$text和$where操作符。
例如,下面的示例使用查询表达式指定验证器规则:
- db.createCollection( "contacts",
- { validator: { $or:
- [
- { phone: { $type: "string" } },
- { email: { $regex: /@mongodb\.com$/ } },
- { status: { $in: [ "Unknown", "Incomplete" ] } }
- ]
- }
- } )
4. 性能
验证发生在更新和插入期间。当您将验证添加到集合时,现有文档在修改之前不会进行验证检查。
4.1 现有文献
validationLevel选项决定哪些操作MongoDB应用验证规则:
例如,使用以下文档创建联系人集合:
- db.contacts.insert([
- { "_id": 1, "name": "Anne", "phone": "+1 555 123 456", "city": "London", "status": "Complete" },
- { "_id": 2, "name": "Ivan", "city": "Vancouver" }
- ])
发出以下命令将验证器添加到联系人集合:
- db.runCommand( {
- collMod: "contacts",
- validator: { $jsonSchema: {
- bsonType: "object",
- required: [ "phone", "name" ],
- properties: {
- phone: {
- bsonType: "string",
- description: "must be a string and is required"
- },
- name: {
- bsonType: "string",
- description: "must be a string and is required"
- }
- }
- } },
- validationLevel: "moderate"
- } )
联系人集合现在有一个验证器,其验证级别为中等:
要完全禁用验证,可以将validationLevel设置为off。
4.2 接受或拒绝无效的文档
validationAction选项决定MongoDB如何处理违反验证规则的文档:
例如,使用以下JSON模式验证器创建contacts2集合:
- db.createCollection( "contacts2", {
- validator: { $jsonSchema: {
- bsonType: "object",
- required: [ "phone" ],
- properties: {
- phone: {
- bsonType: "string",
- description: "must be a string and is required"
- },
- email: {
- bsonType : "string",
- pattern : "@mongodb\.com$",
- description: "must be a string and match the regular expression pattern"
- },
- status: {
- enum: [ "Unknown", "Incomplete" ],
- description: "can only be one of the enum values"
- }
- }
- } },
- validationAction: "warn"
- } )
使用warn validationAction, MongoDB会记录任何违规行为,但允许继续进行插入或更新。
例如,下面的插入操作违反了验证规则:
db.contacts2.insert( { name: "Amanda", status: "Updated" } )
但是,由于validationAction仅发出警告,所以MongoDB只记录验证冲突消息,并允许操作继续:
2017-12-01T12:31:23.738-0500 W STORAGE [conn1] Document would fail validation collection: example.contacts2 doc: { _id: ObjectId('5a2191ebacbbfc2bdc4dcffc'), name: "Amanda", status: "Updated" }
5. 限制条件
不能在管理、本地和配置数据库中为集合指定验证器。
不能为system.*
collections指定验证器。
6. 绕过文档验证
用户可以使用bypassDocumentValidation选项绕过文档验证。
下面的命令可以使用新的选项bypassDocumentValidation绕过每个操作的验证:
命令和db.collection.findAndModify()方法mapReduce命令和db.collection.mapReduce()方法插入命令更新命令
$out和$merge阶段用于聚合命令和db.collection.aggregate()方法
对于已启用访问控制的部署,要绕过文档验证,经过身份验证的用户必须具有bypassDocumentValidation操作。内置角色dbAdmin和restore提供此操作。
三、数据模型设计
有效的数据模型支持您的应用程序需求。文档结构的关键考虑因素是嵌入还是使用引用。
1.嵌入式数据模型
使用MongoDB,您可以将相关数据嵌入到单个结构或文档中。这些模式通常被称为“非规范化”模型,并利用了MongoDB丰富的文档。考虑以下图表:
嵌入式数据模型允许应用程序在同一数据库记录中存储相关信息。因此,应用程序可能需要发出更少的查询和更新来完成公共操作。
一般来说,在以下情况下使用嵌入式数据模型:
通常,嵌入为读取操作提供了更好的性能,以及在单个数据库操作中请求和检索相关数据的能力。嵌入式数据模型使得在单个原子写操作中更新相关数据成为可能。
要访问嵌入文档中的数据,请使用点符号“进入”嵌入文档。有关访问数组和嵌入文档中的数据的更多示例,请参见查询数组中的数据和查询嵌入文档中的数据。
1.1 嵌入式数据模型和文档大小限制
MongoDB中的文档必须小于最大BSON文档大小。
对于大容量二进制数据,请考虑GridFS。
2. 规范化数据模型
规范化数据模型使用文档之间的引用来描述关系。
一般来说,使用规范化的数据模型:
要加入集合,MongoDB提供了聚合阶段:
MongoDB还提供了跨集合连接数据的引用。
有关规范化数据模型的示例,请参阅具有文档引用的模型一对多关系。
有关各种树模型的示例,请参见模型树结构。
四、操作因素和数据模型
为MongoDB建模应用程序数据应该考虑影响MongoDB性能的各种操作因素。例如,不同的数据模型可以实现更有效的查询,增加插入和更新操作的吞吐量,或者更有效地将活动分配到切分集群。
在开发数据模型时,请结合以下考虑因素分析应用程序的所有读写操作。
1.原子性
在MongoDB中,写操作是单个文档级别上的原子操作,即使该操作修改了单个文档中的多个嵌入文档。当一个写操作修改多个文档时(例如db.collection.updateMany()),每个文档的修改都是原子性的,但是整个操作不是原子性的。
1.1 嵌入式数据模型
嵌入式数据模型将所有相关数据合并到一个文档中,而不是跨多个文档和集合进行规范化。这个数据模型简化了原子操作。
有关为单个文档提供原子更新的示例数据模型,请参阅原子操作的模型数据。
1.2 多文档事务
对于在相关数据块之间存储引用的数据模型,应用程序必须发出单独的读和写操作来检索和修改这些相关数据块。
对于需要对多个文档进行原子性读写的情况(在单个或多个集合中),MongoDB支持多文档事务:
重要的:
在大多数情况下,与单个文档写入相比,多文档事务会带来更大的性能成本,而且多文档事务的可用性不应该代替有效的模式设计。对于许多场景,非规范化数据模型(嵌入文档和数组)对于您的数据和用例仍然是最优的。也就是说,对于许多场景,适当地对数据建模将最小化对多文档事务的需求。
有关其他事务使用注意事项(如运行时限制和oplog大小限制),请参见生产
2. 分区
MongoDB使用分片来提供水平扩展。这些集群支持具有大数据集和高吞吐量操作的部署。切分允许用户在数据库中对一个集合进行分区,以便在多个mongod实例或碎片之间分发集合的文档。
为了在分片集合中分布数据和应用程序流量,MongoDB使用分片键。选择适当的切分键对性能有重要影响,可以启用或防止查询隔离和增加写容量。重要的是要仔细考虑用作碎片键的字段。
3.索引
使用索引来提高普通查询的性能。在查询中经常出现的字段和返回排序结果的所有操作上构建索引。MongoDB自动在_id字段上创建一个惟一的索引。
在创建索引时,请考虑以下索引行为:
有关索引的更多信息,以及分析查询性能,请参见索引策略。此外,MongoDB数据库分析器可以帮助识别低效的查询。
4. 大量收集
在某些情况下,您可能选择将相关信息存储在多个集合中,而不是单个集合中。
考虑一个示例收集日志,它存储各种环境和应用程序的日志文档。日志集合包含以下形式的文档:
- { log: "dev", ts: ..., info: ... }
- { log: "debug", ts: ..., info: ...}
如果文档总数较低,则可以按类型将文档分组到集合中。对于日志,考虑维护不同的日志集合,比如logs_dev和logs_debug。logs_dev集合只包含与dev环境相关的文档。
通常,拥有大量的集合不会带来显著的性能损失,并且会带来非常好的性能。不同的集合对于高吞吐量的批处理非常重要。
当使用具有大量集合的模型时,请考虑以下行为:
5. 集合包含大量的小文档
如果集合中有大量的小文档,出于性能考虑,应该考虑嵌入。如果您可以按照某种逻辑关系将这些小文档分组,并且经常按照这种分组检索文档,那么您可以考虑将这些小文档“汇总”成包含嵌入文档数组的较大文档。
将这些小文档“上卷”到逻辑分组中意味着检索一组文档的查询包括顺序读取和更少的随机磁盘访问。此外,“卷起”文档并将通用字段移动到更大的文档对这些字段上的索引有好处。公共字段的副本会更少,相应索引中的关联键条目也会更少。有关索引的更多信息,请参见索引。
但是,如果您经常只需要检索组中的文档子集,那么“汇总”文档可能不会提供更好的性能。此外,如果小的、独立的文档表示数据的自然模型,则应该维护该模型。
6. 小型文档的存储优化
每个MongoDB文档都包含一定的开销。这种开销通常是微不足道的,但是如果所有文档都只有几个字节,那么这种开销就会变得非常大,就像如果集合中的文档只有一个或两个字段时的情况一样。
为优化这些集合的存储利用率,请考虑以下建议和策略:
MongoDB客户端会自动向每个文档添加一个_id字段,并为_id字段生成惟一的12字节ObjectId。而且,MongoDB总是索引_id字段。对于较小的文档,这可能会占用大量的空间。
为了优化存储使用,用户可以在将文档插入集合时显式地为_id字段指定一个值。此策略允许应用程序在_id字段中存储一个值,该值将占用文档另一部分的空间。
您可以在_id字段中存储任何值,但是因为这个值是集合中文档的主键,所以它必须惟一地标识它们。如果字段的值不是唯一的,那么它就不能作为主键,因为集合中会有冲突。
请注意:
缩短字段名会降低表达性,对较大的文档没有很大的好处,而且文档开销也不是很重要。较短的字段名不会减少索引的大小,因为索引具有预定义的结构。
通常,没有必要使用短字段名。
MongoDB在每个文档中存储所有字段名。对于大多数文档来说,这只是文档所使用空间的一小部分;但是,对于小文档,字段名可能表示成比例的大空间。考虑一个类似以下的小文档集合:
{ last_name : "Smith", best_score: 3.9 }
如果将名为last_name的字段缩短为lname,将名为best_score的字段缩短为score,如下所示,每个文档可以节省9个字节。
{ lname : "Smith", score : 3.9 }
在某些情况下,您可能希望将文档嵌入到其他文档中,从而节省每个文档的开销。“查看”集合包含大量小文档。
7.数据生命周期管理
数据建模决策应该考虑数据生命周期管理。
集合的生存时间或TTL特性在一段时间后过期。如果您的应用程序需要一些数据在数据库中保存一段有限的时间,那么可以考虑使用TTL特性。
另外,如果您的应用程序只使用最近插入的文档,那么可以考虑使用上限集合。Capped集合提供了对插入文档的先进先出(FIFO)管理,并有效地支持基于插入顺序插入和读取文档的操作。
五、数据模型示例和模式
1.文档之间的模型关系
1.1 对嵌入文档的一对一关系建模
概述
此页面描述一个数据模型,该模型使用嵌入式文档来描述连接数据之间的一对一关系。
模式
考虑以下映射用户和地址关系的示例。这个例子说明了如果需要在一个数据实体的上下文中查看另一个数据实体,那么嵌入优于引用的优点。在patron和address数据之间的这种一对一关系中,address属于patron。
在规范化数据模型中,address文档包含对patron文档的引用。
- {
- _id: "joe",
- name: "Joe Bookreader"
- }
-
- {
- patron_id: "joe",
- street: "123 Fake Street",
- city: "Faketon",
- state: "MA",
- zip: "12345"
- }
如果经常使用名称信息检索地址数据,那么通过引用,您的应用程序需要发出多个查询来解析引用。更好的数据模型是将地址数据嵌入到用户数据中,如下文所示:
- {
- _id: "joe",
- name: "Joe Bookreader",
- address: {
- street: "123 Fake Street",
- city: "Faketon",
- state: "MA",
- zip: "12345"
- }
- }
使用嵌入式数据模型,您的应用程序可以通过一个查询检索完整的用户信息。
1.2 对嵌入文档的一对多关系建模
概述
此页面描述一个数据模型,该模型使用嵌入式文档来描述连接数据之间的一对多关系。
模式
考虑以下映射patron和多个地址关系的示例。这个例子说明了如果您需要查看另一个上下文中的许多数据实体,那么嵌入优于引用的优点。在用户和地址数据之间的一对多关系中,用户有多个地址实体。
在规范化数据模型中,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"
- }
如果您的应用程序经常检索带有名称信息的地址数据,那么您的应用程序需要发出多个查询来解析引用。更理想的模式是将地址数据实体嵌入到用户数据中,如下文所示:
- {
- _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"
- }
- ]
- }
使用嵌入式数据模型,您的应用程序可以通过一个查询检索完整的用户信息。
1.3 使用文档引用建模一对多关系
概述
此页面描述一个数据模型,该模型使用文档之间的引用来描述连接数据之间的一对多关系。
模式
考虑以下映射出版商和图书关系的示例。这个例子说明了引用比嵌入的优点,从而避免了发布方信息的重复。
将publisher文档嵌入到图书文档中会导致出版商数据的重复,如下面的文档所示:
- {
- 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"
-
- }
-
- }
为了避免出版商数据的重复,请使用参考资料,并将出版商信息与图书收藏分开保存。
在使用引用时,关系的增长将决定在何处存储引用。如果每个出版商的图书数量很少,增长有限,那么将图书引用存储在publisher文档中有时可能有用。否则,如果每个出版商的图书数量是无界的,则此数据模型将导致可变的、不断增长的数组,如下面的示例所示:
- {
- name: "O'Reilly Media",
- founded: 1980,
- location: "CA",
-
- books: [123456789, 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"
- }
为了避免可变的、增长的数组,将publisher引用存储在图书文档中:
- {
- _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"
-
- }
2.模型树结构
MongoDB允许各种方式使用树数据结构来建模大型分层或嵌套的数据关系。
2.1 使用父级引用对树结构建模
概述
这个页面描述了一个数据模型,它通过在子节点中存储对“父”节点的引用来描述MongoDB文档中的树状结构。
模型
父引用模式将每个树节点存储在文档中;除了树节点之外,文档还存储节点的父节点的id。
考虑以下类别的层次结构:
下面的示例使用父类引用对树进行建模,并将引用存储到父类字段中的父类:
- db.categories.insert( { _id: "MongoDB", parent: "Databases" } )
- db.categories.insert( { _id: "dbm", parent: "Databases" } )
- db.categories.insert( { _id: "Databases", parent: "Programming" } )
- db.categories.insert( { _id: "Languages", parent: "Programming" } )
- db.categories.insert( { _id: "Programming", parent: "Books" } )
- db.categories.insert( { _id: "Books", parent: null } )
a.检索节点的父节点的查询是快速和直接的:
db.categories.findOne( { _id: "MongoDB" } ).parent
b.你可以在父字段上创建一个索引,以支持通过父节点进行快速搜索:
db.categories.createIndex( { parent: 1 } )
c.你可以通过查询父字段找到它的直接子节点:
db.categories.find( { parent: "Databases" } )
2.2 使用子引用对树结构建模
概述
这个页面描述了一个数据模型,它通过将父节点中的引用存储到子节点来描述MongoDB文档中的树状结构。
模型
子引用模式将每个树节点存储在文档中;除了树节点之外,文档还将节点的子节点的id存储在一个数组中。
考虑以下类别的层次结构:
下面的示例使用子引用对树进行建模,并在子字段中存储对节点子字段的引用:
- db.categories.insert( { _id: "MongoDB", children: [] } )
- db.categories.insert( { _id: "dbm", children: [] } )
- db.categories.insert( { _id: "Databases", children: [ "MongoDB", "dbm" ] } )
- db.categories.insert( { _id: "Languages", children: [] } )
- db.categories.insert( { _id: "Programming", children: [ "Databases", "Languages" ] } )
- db.categories.insert( { _id: "Books", children: [ "Programming" ] } )
a.检索节点的直接子节点的查询是快速和直接的:
db.categories.findOne( { _id: "Databases" } ).children
b.您可以在字段子节点上创建一个索引,以支持通过子节点进行快速搜索:
db.categories.createIndex( { children: 1 } )
c.你可以在子字段中查询一个节点,找到它的父节点和它的兄弟节点:
db.categories.find( { children: "MongoDB" } )
只要不需要对子树进行操作,子引用模式就为树存储提供了一个合适的解决方案。此模式还可以为存储节点可能具有多个父节点的图提供合适的解决方案。
2.3 使用祖先数组对树结构建模
概述
这个页面描述了一个数据模型,该模型使用对父节点的引用和存储所有祖先节点的数组来描述MongoDB文档中的树状结构。
模型
祖先模式数组将每个树节点存储在文档中;除了树节点之外,文档还将节点的祖先或路径的id存储在一个数组中。
考虑以下类别的层次结构:
下面的示例使用祖先数组对树进行建模。除了祖先字段外,这些文档还在父字段中存储对直接父类别的引用:
- db.categories.insert( { _id: "MongoDB", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" } )
- db.categories.insert( { _id: "dbm", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" } )
- db.categories.insert( { _id: "Databases", ancestors: [ "Books", "Programming" ], parent: "Programming" } )
- db.categories.insert( { _id: "Languages", ancestors: [ "Books", "Programming" ], parent: "Programming" } )
- db.categories.insert( { _id: "Programming", ancestors: [ "Books" ], parent: "Books" } )
- db.categories.insert( { _id: "Books", ancestors: [ ], parent: null } )
a.检索节点的祖先或路径的查询是快速和直接的:
db.categories.findOne( { _id: "MongoDB" } ).ancestors
b.你可以创建一个索引字段的祖先,使快速搜索由祖先节点:
db.categories.createIndex( { ancestors: 1 } )
c.你可以通过字段祖先查询找到它的所有后代:
db.categories.find( { ancestors: "Programming" } )
祖先数组模式通过在祖先字段的元素上创建索引,为查找节点的后代和祖先提供了快速有效的解决方案。这使得祖先数组成为处理子树的一个很好的选择。
祖先数组模式比实体化路径模式稍微慢一些,但是使用起来更简单。
2.4 使用实体化路径对树结构建模
概述
这个页面描述了一个数据模型,它通过存储文档之间的完整关系路径来描述MongoDB文档中的树状结构。
模型
物化路径模式将每个树节点存储在一个文档中;除了树节点之外,文档还以字符串的形式存储节点的祖先或路径的id。虽然物化路径模式需要处理字符串和正则表达式的额外步骤,但该模式在处理路径方面也提供了更大的灵活性,比如通过部分路径查找节点。
考虑以下类别的层次结构:
下面的示例使用实体化路径对树进行建模,并将路径存储在字段路径中;路径字符串使用逗号作为分隔符:
- db.categories.insert( { _id: "Books", path: null } )
- db.categories.insert( { _id: "Programming", path: ",Books," } )
- db.categories.insert( { _id: "Databases", path: ",Books,Programming," } )
- db.categories.insert( { _id: "Languages", path: ",Books,Programming," } )
- db.categories.insert( { _id: "MongoDB", path: ",Books,Programming,Databases," } )
- db.categories.insert( { _id: "dbm", path: ",Books,Programming,Databases," } )
a.可以查询检索整棵树,按字段路径排序:
db.categories.find().sort( { path: 1 } )
b.你可以在path字段上使用正则表达式来找到编程的后代:
db.categories.find( { path: /,Programming,/ } )
c.您还可以检索图书的后代,其中的图书也在层次结构的最顶层:
db.categories.find( { path: /^,Books,/ } )
d.要在字段路径上创建索引,请使用以下调用:
db.categories.createIndex( { path: 1 } )
这个索引可以提高性能取决于查询:
对于这些查询,如果索引比整个集合小得多,则索引可以提供一些性能改进。
2.5 使用嵌套集对树结构建模
概述
本文档描述了一个数据模型,该模型描述了一个类似于树的结构,该结构以树的可变性为代价来优化发现子树。
模型
嵌套的set模式将树中的每个节点标识为树的往返遍历的停止点。应用程序访问树中的每个节点两次;第一次是在最初的行程中,第二次是在返回行程中。嵌套的集合模式将每个树节点存储在文档中;除了树节点之外,文档还在左侧字段中存储节点的父节点的id、节点的初始停止,在右侧字段中存储节点的返回停止。
考虑以下类别的层次结构:
下面的示例使用嵌套集对树进行建模:
- db.categories.insert( { _id: "Books", parent: 0, left: 1, right: 12 } )
- db.categories.insert( { _id: "Programming", parent: "Books", left: 2, right: 11 } )
- db.categories.insert( { _id: "Languages", parent: "Programming", left: 3, right: 4 } )
- db.categories.insert( { _id: "Databases", parent: "Programming", left: 5, right: 10 } )
- db.categories.insert( { _id: "MongoDB", parent: "Databases", left: 6, right: 7 } )
- db.categories.insert( { _id: "dbm", parent: "Databases", left: 8, right: 9 } )
您可以查询检索节点的后代:
- var databaseCategory = db.categories.findOne( { _id: "Databases" } );
- db.categories.find( { left: { $gt: databaseCategory.left }, right: { $lt: databaseCategory.right } } );
嵌套的集合模式为查找子树提供了一种快速而有效的解决方案,但是对于修改树结构来说效率很低。因此,这种模式最适合不改变的静态树。
3. 特定于模型的应用程序上下文
3.1 原子操作的模型数据
虽然MongoDB支持副本集(从4.0版本开始)和分片集群(从4.2版本开始)的多文档事务,但是对于许多场景,正如本文所讨论的,非规范化数据模型对于您的数据和用例仍然是最优的。
概述
虽然MongoDB支持副本集(从4.0版本开始)和分片集群(从4.2版本开始)的多文档事务,但是对于许多场景,正如本文所讨论的,非规范化数据模型对于您的数据和用例仍然是最优的。
图书的可用副本和结帐信息应该是同步的。因此,将available字段和checkout字段嵌入到同一个文档中可以确保自动更新两个字段。
- {
- _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",
-
- available: 3,
-
- checkout: [ { by: "joe", date: ISODate("2012-10-15") } ]
-
- }
然后,要使用新的签出信息进行更新,可以使用db. collections . updateone()方法自动更新可用字段和签出字段:
- db.books.updateOne (
- { _id: 123456789, available: { $gt: 0 } },
- {
- $inc: { available: -1 },
- $push: { checkout: { by: "abc", date: new Date() } }
- }
- )
操作返回一个包含操作状态信息的文档:
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
matchedCount字段显示一个文档匹配更新条件,modifiedCount显示操作更新了一个文档。
如果没有文档匹配更新条件,则matchedCount和modifiedCount将为0,表示您不能签出图书。
3.2 模型数据支持关键字搜索
请注意:
关键字搜索不同于文本搜索或全文搜索,不提供词干分析或其他文本处理功能。有关更多信息,请参见关键字索引的限制一节。
在2.4中,MongoDB提供了文本搜索功能。有关更多信息,请参见文本索引。
如果您的应用程序需要对包含文本的字段的内容执行查询,您可以对文本执行精确匹配,或者使用$regex来使用正则表达式模式匹配。但是,对于文本上的许多操作,这些方法不能满足应用程序的要求。
该模式描述了一种使用MongoDB支持关键字搜索的方法,该方法使用存储在与文本字段相同的文档中的数组中的关键字来支持应用程序搜索功能。与多键索引相结合,此模式可以支持应用程序的关键字搜索操作。
模型
要向文档中添加结构以支持基于关键字的查询,请在文档中创建一个数组字段,并将关键字作为字符串添加到数组中。然后可以在数组上创建一个多键索引,并创建从数组中选择值的查询。
例子:
给定希望提供基于主题的搜索的图书馆卷集合。对于每个卷,添加数组主题,并根据给定卷添加所需的关键字。
对于Moby-Dick,你可能有以下文件:
- { title : "Moby-Dick" ,
- author : "Herman Melville" ,
- published : 1851 ,
- ISBN : 0451526996 ,
- topics : [ "whaling" , "allegory" , "revenge" , "American" ,
- "novel" , "nautical" , "voyage" , "Cape Cod" ]
- }
然后在主题数组上创建一个多键索引:
db.volumes.createIndex( { topics: 1 } )
多键索引为主题数组中的每个关键字创建单独的索引项。例如,该索引包含一个捕鲸条目和一个寓言条目。
然后根据关键字进行查询。例如:
db.volumes.findOne( { topics : "voyage" }, { title: 1 } )
请注意:
一个包含大量元素的数组(比如包含数百或数千个关键字的数组)在插入时将产生更大的索引开销。
关键字索引的限制
MongoDB可以支持使用特定的数据模型和多键索引的关键字搜索;但是,这些关键字索引在以下方面还不够充分或无法与全文本产品相比:
3.3 模型的货币数据
概述
处理货币数据的应用程序通常需要能够捕获货币的小数单位,并且在执行算术时需要模拟精确的十进制舍入。许多现代系统使用的基于二进制的浮点运算。(如float, double)不能表示精确的小数部分,需要一定程度的近似值,因此不适合用于货币算术。在建模货币数据时,此约束是一个重要的考虑因素。
在MongoDB中有几种使用数值和非数值模型来建模货币数据的方法。
(1)数值模型
如果您需要查询数据库以获得精确的、数学上有效的匹配,或者需要执行服务器端算术,例如$inc、$mul和聚合框架算术,那么数字模型可能是合适的。
a.以下方法遵循数值模型:
b.非数字模型
如果不需要对货币数据执行服务器端算术,或者服务器端近似足够,那么使用非数值模型建模货币数据可能是合适的。
c.下面的方法遵循非数值模型:
请注意:
本页提到的算术是指mongod或mongos执行的服务器端算术,不是指客户端算术。
(2)数值模型
a.使用Decimal BSON类型
新版本3.4。
decimal BSON类型使用IEEE 754 decimal128基于小数的浮点编号格式。不像基于二进制的浮点格式。decimal128不能近似十进制值,但能够提供处理货币数据所需的准确精度。
从mongo shell中分配十进制值并使用NumberDecimal()构造函数进行查询。以下示例将包含天然气价格的文档添加到天然气价格集合:
db.gasprices.insert{ "_id" : 1, "date" : ISODate(), "price" : NumberDecimal("2.099"), "station" : "Quikstop", "grade" : "regular" }
下面的查询与上面的文档匹配:
db.gasprices.find( { price: NumberDecimal("2.099") } )
b.将值转换为小数
通过执行一次性转换或修改应用程序逻辑在访问记录时执行转换,可以将集合的值转换为十进制类型。
提示:
除了下面列出的步骤之外,从版本4.0开始,您可以使用$convert及其助手$toDecimal操作符将值转换为NumberDecimal()。
c.一次性收集转换
可以通过遍历集合中的所有文档、将货币值转换为十进制类型并将文档写回集合来转换集合。
请注意:
强烈建议将decimal值作为新字段添加到文档中,并在稍后验证新字段的值后删除旧字段。
警告:
确保在独立的测试环境中测试十进制转换。一旦使用MongoDB 3.4版本创建或修改数据文件,它们将不再与以前的版本兼容,并且不支持降级包含小数的数据文件。
d.比例因子转换:
考虑以下集合,它使用比例因子方法,并将货币值保存为表示美分数的64位整数:
- { "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong("1999") },
- { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong("3999") },
- { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong("2999") },
- { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong("2495") },
- { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong("8000") }
通过使用$multiply操作符将price和NumberDecimal(“0.01”)相乘,可以将长值转换为适当格式的小数值。下面的聚合管道将转换后的值分配到$addFields阶段的新priceDec字段:
- db.clothes.aggregate(
- [
- { $match: { price: { $type: "long" }, priceDec: { $exists: 0 } } },
- {
- $addFields: {
- priceDec: {
- $multiply: [ "$price", NumberDecimal( "0.01" ) ]
- }
- }
- }
- ]
- ).forEach( ( function( doc ) {
- db.clothes.save( doc );
- } ) )
可以使用db.clothes.find()查询来验证聚合管道的结果:
- { "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong(1999), "priceDec" : NumberDecimal("19.99") }
- { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong(3999), "priceDec" : NumberDecimal("39.99") }
- { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong(2999), "priceDec" : NumberDecimal("29.99") }
- { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong(2495), "priceDec" : NumberDecimal("24.95") }
- { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong(8000), "priceDec" : NumberDecimal("80.00") }
如果不希望添加具有十进制值的新字段,则可以覆盖原始字段。下面的update()方法首先检查price是否存在,它是一个long值,然后将long值转换为decimal,并将其存储在price字段中:
- db.clothes.update(
- { price: { $type: "long" } },
- { $mul: { price: NumberDecimal( "0.01" ) } },
- { multi: 1 }
- )
可以使用db.clothes.find()查询来验证结果:
- { "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberDecimal("19.99") }
- { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberDecimal("39.99") }
- { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberDecimal("29.99") }
- { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberDecimal("24.95") }
- { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberDecimal("80.00") }
非数字的转换:
考虑下面的集合,它使用了非数值模型,并将货币值保存为一个字符串,该字符串具有该值的精确表示形式:
- { "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99" }
- { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99" }
- { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99" }
- { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95" }
- { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00" }
下面的函数首先检查price是否存在,它是否是一个字符串,然后将字符串值转换为十进制值,并将其存储在priceDec字段中:
- db.clothes.find( { $and : [ { price: { $exists: true } }, { price: { $type: "string" } } ] } ).forEach( function( doc ) {
- doc.priceDec = NumberDecimal( doc.price );
- db.clothes.save( doc );
- } );
该函数不向命令行输出任何内容。可以使用db.clothes.find()查询来验证结果:
- { "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99", "priceDec" : NumberDecimal("19.99") }
- { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99", "priceDec" : NumberDecimal("39.99") }
- { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99", "priceDec" : NumberDecimal("29.99") }
- { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95", "priceDec" : NumberDecimal("24.95") }
- { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00", "priceDec" : NumberDecimal("80.00") }
应用程序逻辑转换
可以在应用程序逻辑中执行对十进制类型的转换。在这个场景中,应用程序在访问记录时修改以执行转换。
典型的应用逻辑如下:
使用比例因子
请注意:
如果您使用的是MongoDB 3.4或更高版本,那么使用十进制类型来建模货币数据比使用比例因子方法更好。
使用比例因子方法对货币数据建模:
例如,下列比例尺为9.99美元/ 1000,以保持精度不超过0.1美分。
{ price: 9990, currency: "USD" }
该模型假设给定货币价值:
在使用此模型时,应用程序必须在执行适当的值缩放时保持一致。
非数字模型
若要使用非数值模型对货币数据建模,请将值存储在两个字段中:
下面的例子使用非数字模型来存储9.99美元的价格和0.25美元的费用:
- {
- price: { display: "9.99", approx: 9.9900000000000002, currency: "USD" },
- fee: { display: "0.25", approx: 0.2499999999999999, currency: "USD" }
- }
只要稍加注意,应用程序就可以使用数值近似值对字段执行范围和排序查询。但是,在查询和排序操作中使用逼近字段要求应用程序执行客户端后处理来解码精确值的非数字表示,然后根据精确的货币值过滤出返回的文档。
3.4 模型时间数据
概述
默认情况下,MongoDB使用UTC存储时间,并将任何本地时间表示形式转换成这种形式。必须对未修改的本地时间值进行操作或报告的应用程序可以将时区存储在UTC时间戳旁边,并在其应用程序逻辑中计算原始的本地时间。
例子
在MongoDB shell中,可以存储当前日期和当前客户端的UTC偏移量。
- var now = new Date();
- db.data.save( { date: now,
- offset: now.getTimezoneOffset() } );
你可以通过应用保存的偏移量来重建原始的本地时间:
- var record = db.data.findOne();
- var localNow = new Date( record.date.getTime() - ( record.offset * 60000 ) );
4. 数据库的引用
对于MongoDB中的许多用例,将相关数据存储在单个文档中的非规范化数据模型将是最优的。然而,在某些情况下,将相关信息存储在单独的文档中是有意义的,通常存储在不同的集合或数据库中。
重要的:
MongoDB 3.2引入了$lookup管道阶段,用于对同一个数据库中的未分片集合执行左外连接。有关更多信息和示例,请参见$lookup。
从MongoDB 3.4开始,您还可以使用$graphLookup pipeline stage连接一个未分片的集合来执行递归搜索。有关更多信息和示例,请参见$graphLookup。
此页面概述了在$lookup和$graphLookup管道阶段之前的其他过程。
MongoDB应用程序使用两种方法中的一种来关联文档:
除非您有令人信服的理由使用DBRefs,否则请使用手动引用。
[1]一些社区支持的驱动程序可能具有替代行为,并可能自动将DBRef解析为文档。
4.1 手动引用
a.背景
使用手动引用是在另一个文档中包含一个文档的_id字段的实践。然后,应用程序可以根据需要发出第二个查询来解析引用的字段。
b.流程
考虑下面插入两个文档的操作,使用第一个文档的_id字段作为第二个文档中的引用:
- original_id = ObjectId()
-
- db.places.insert({
- "_id": original_id,
- "name": "Broadway Center",
- "url": "bc.example.net"
- })
-
- db.people.insert({
- "name": "Erin",
- "places_id": original_id,
- "url": "bc.example.net/Erin"
- })
然后,当查询返回人员集合中的文档时,如果需要,可以对places集合中的places_id字段引用的文档进行第二个查询。
c.使用
对于几乎所有希望存储两个文档之间关系的情况,都要使用手动引用。创建引用很简单,您的应用程序可以根据需要解析引用。
手动链接的惟一限制是这些引用不传递数据库和集合的名称。如果单个集合中的文档与多个集合中的文档相关,则可能需要考虑使用DBRefs。
4.2 DBRefs
a.背景
DBRefs是表示文档的约定,而不是特定的引用类型。除了_id字段的值之外,它们还包括集合的名称,在某些情况下还包括数据库名称。
b.格式
dbref有以下字段:
$ ref
$ref字段保存引用文档所在的集合的名称。
$ id
$id字段包含引用文档中的_id字段的值。
$ db
可选的。
包含引用文档所在的数据库的名称。
只有一些驱动程序支持$db引用。
例子:
DBRef文件类似于以下文件:
{ "$ref" : <value>, "$id" : <value>, "$db" : <value> }
考虑存储在creator字段中的DBRef集合中的文档:
- {
- "_id" : ObjectId("5126bbf64aed4daf9e2ab771"),
- // .. application fields
- "creator" : {
- "$ref" : "creators",
- "$id" : ObjectId("5126bc054aed4daf9e2ab772"),
- "$db" : "users"
- }
- }
本例中的DBRef指向用户数据库创建者集合中的一个文档,该文档的_id字段中有ObjectId(“5126bc054aed4daf9e2ab772”)。
请注意:
DBRef中字段的顺序很重要,在使用DBRef时必须使用上面的顺序。
Driver | DBRef Support | Notes |
---|---|---|
C | Not Supported | You can traverse references manually. |
C++ | Not Supported | You can traverse references manually. |
C# | Supported | Please see the C# driver page for more information. |
Haskell | Not Supported | You can traverse references manually. |
Java | Supported | Please see the Java driver page for more information. |
Node.js | Supported | Please see the Node.js driver page for more information. |
Perl | Supported | Please see the Perl driver page for more information. |
PHP | Not Supported | You can traverse references manually. |
Python | Supported | Please see the PyMongo driver page for more information. |
Ruby | Supported | Please see the Ruby driver page for more information. |
Scala | Not Supported | You can traverse references manually. |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。