这种灵活性有助于将文档映射到实体或对象。即使文档与集合中的其他文档有很大差异,每个文档也可以匹配所表示实体的数据字段。
设计基于MongoDB的应用程序的数据模型时的关键就是选择合适的文档结构以及确定应用程序如何描述数据之间的关系。有两种方式可以用来描述这些关系: 引用
及内嵌
内嵌方式指的是把相关联的数据保存在同一个文档结构之内。MongoDB的文档结构允许一个字段或者一个数组内的值为一个嵌套的文档。这种 冗余 的数据模型可以让应用程序在一个数据库操作内完成对相关数据的读取或修改。这样一来,应用程序就可以发送较少的请求给MongoDB数据库来完成常用的查询及更新请求。
contains(包含)
关系。 参见一对一关系建模:内嵌文档模型。一对多
的关系。 这些情况下 “多个”或者子文档会经常和父文档一起被显示和查看。请参见 一对多关系建模: 内嵌文档模型。通常情况下,内嵌数据会对读操作
有比较好的性能提高,也可以使应用程序在一个单个操作就可以完成对数据的读取。 同时,内嵌数据也对更新相关数据提供了一个原子性写操作。
MongoDB中的文档必须小于最大BSON文档大小。对于批量二进制数据,请考虑GridFS。
规范化数据模型指的是通过使用引用
来表达对象之间的关系。
一般来说,在下述情况下可以使用规范化模型:
重复
,并且读性能的优势又不足于盖过数据重复的弊端时候。多对多关系
的时候。多层次
结构数据集。引用比内嵌要更加灵活一些。 但客户端应用必须使用二次查询
来解析文档内包含的引用。换句话说,对同样的操作来说,规范化模式会导致更多的网络请求发送到数据库服务器端。
MongoDB设计数据模型应考虑影响MongoDB性能的各种操作因素。 例如,不同的数据模型可以允许更有效的查询,增加插入和更新操作的吞吐量,或者更有效地将活动分配给分片集群。
在开发数据模型时,请结合以下注意事项分析所有应用程序的读写操作。
在MongoDB中,即使操作修改单个文档中的多个嵌入文档,写操作在单个文档
的级别上也是原子
操作。当单个写操作修改多个文档(例如db.collection.updateMany())时,每个文档的修改都是原子的,但整个操作不是原子操作。
把相关数据定义到同一个文档里面的内嵌
方式有利于这种原子性操作。对于那些使用引用来关联相关数据的数据模型,应用程序必须再用额外的读和写的操作去取回和修改相关的数据。
单独
的读取和写入操作以检索和修改这些相关的数据片段。从版本4.0开始,对于需要原子性来更新多个文档或读取多个文档之间的一致性的情况,MongoDB为副本集提供了多文档事务
。MongoDB 使用 sharding (分片)来实现水平扩展。使用分片的集群可以支持海量的数据和高并发读写。用户可以使用分片技术把一个数据库内的某一个集合的数据进行分区,从而达到把数据分布到多个 mongod 实例(或分片上)的目的。
Mongodb 依据分片键
分发数据和应用程序的事务请求。选择一个合适的分片键会对性能有很大的影响,也会促进或者阻碍MongoDB的定向分片查询和增强的写性能。所以在选择分片键时候要仔细考量分片键所用的字段。
对常用操作可以使用索引来提高性能。对查询条件中常见的字段,以及需要排序的字段创建索引。MongoDB会对 _id 字段自动创建唯一索引。
创建索引时,需要考虑索引的下述特征:
在某些情况下,你可能会考虑把相关的数据保存到多个而不是一个集合里面。
一般来说,很大的集合数量对性能没有什么影响,反而在某些场景下有不错的性能。使用不同的集合在高并发批处理场景下会有很好的帮助。
当使用有大量集合的数据模型时,请注意一下几点:
For the MMAPv1 storage engine, namespace files can be no larger than
2047 megabytes. By default namespace files are 16 megabytes. You can
configure the size using the nsSize option. The WiredTiger storage
engine is not subject to this limitation.
考虑顾客和顾客地址的关系模型,使用引用的方式建模
{
_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"
}
}
如果地址address信息经常和顾客的name字段一起被查询,很明显使用内嵌关系更好,应用程序不需要发出额外的请求去解析并读取父文档。
针对顾客和地址的一对多的关系建模。使用引用关系建模,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信息经常和顾客的name字段一起被查询,使用内嵌关系建模,把 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"
}
]
}
针对于出版社和书籍关系建模
如果使用内嵌关系建模:
{
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: [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"
}
使用文档引用的方式,把出版社的信息保存在一个单独的集合里面。
当使用引用时,文档关系的数量级及增长性会决定我们要在哪里保存引用信息。如果每个出版社所出版的书的数量比较小并且不会增长太多,那么可以在出版社文档里保存所有该出版社所出版的书的引用。反之,如果每个出版社所出版的书籍数量很多或者可能增长很快那么这个书籍引用数组就会不断增长,如下所示:
{
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"
}
要避免可变的增长数组,请将出版社信息引用到书籍文档中:
{
_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"
}
父文档引用 模式用一个文档来表示树的一个节点。每一个文档除了存储节点的信息,同时也保存该节点父节点文档的id值。
下面是一个使用 父文档引用 的例子。在 parent 字段里保存了对上一级分类的引用
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 } )
db.categories.findone({_id:"MomgoDb"}).parent
db.categories.createIndex( { parent: 1 } )
db.categories.find( { parent: "Databases" } )
子文档引用 模式用一个文档来表示树的一个节点。每一个文档除了存储节点的信息,同时也用一个数组
来保存该节点所有子节点的id值。还是上面的树型结构。
使用子文档引用,在父文档的 children 字段里保存了对所有下一级分类节点的引用。
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" ] } )
db.categories.findone({_id:"Database"}).children
db.categories.createIndex( { children: 1 } )
db.categories.find({children:"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 } )
ancestors字段按级存储了先祖节点,parent字段则保存对父节点的引用
db.categories.findOne( { _id: "MongoDB" } ).ancestors
db.categories.createIndex( { ancestors: 1 } )
db.categories.find( { ancestors: "Programming" } )
物化路径模式将每个树节点存储在文档中; 除了树节点之外,文档还将节点的祖先或路径的id存储为字符串。 虽然物化路径模式需要使用字符串和正则表达式的其他步骤,但该模式还为处理路径提供了更大的灵活性,例如通过部分路径查找节点。
物化路径来建模的例子。在节点文档中的 path 字段保存了以逗号为分隔符的路径字符串:
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," } )
db.categories.find().sort( { path: 1 } )
db.categories.find( { path: /,Programming,/ } )
db.categories.find( { path: /^,Books,/ } )
db.categories.createIndex( { path: 1 } )
这个索引对某些查询的性能会有所提高:
如果从根节点开始查询,如( /^,Books,/ )或 (/^,Books,Programming,/) path 字段上的索引会对提高查询性能有显著的作用。
如果查询类似于 Programming 这些非根节点下面的子代节点, (/,Databases,/)或类似的子树查询,由于这些被查询的节点可能在索引字符串的中部而导致全索引扫描。
对于这些查询,如果索引明显小于整个集合,则索引可以提供一些性能改进。
嵌套集合 模式对整个树结构进行一次深度优先的遍历。遍历时候对每个节点的压栈和出栈作为两个不同的步骤记录下来。然后每一个节点就是一个文档,除了节点信息外,文档还保存父节点的id以及遍历的两个步骤编号。压栈时的步骤编号保存到 left 字段里, 而出栈时的步骤编号则保存到 right 字段里。
使用嵌套集合的例子:
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 } } );
嵌套集模式为查找子树提供了快速有效的解决方案,但对于可修改的树结构效率很低。 因此,此模式最适用于不更改的静态树
。
尽管MongoDB支持从4.0版本开始的多文档事务
,但对于许多场景,非规范化数据模型将继续为您的数据和用例提供最佳选择。
在MongoDB中,对单个文档的写操作是原子的。 对于必须一起更新的字段,在同一文档中嵌入字段可确保可以原子方式更新字段。
举例来说,假设你在设计一个图书馆的借书系统,需要管理书的库存量以及出借记录。
一本书的可借数量加上借出数量的和必须等于总的保有量,那么对这两个字段的更新必须是原子性的。把 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.collection.update() 的方法来对 available 和 checkout 两个字段同时更新:
db.books.updateOne (
{ _id: 123456789, available: { $gt: 0 } },
{
$inc: { available: -1 },
$push: { checkout: { by: "abc", date: new Date() } }
}
)
如果应用需要对某个文本字段进行查询,可以用完全匹配或使用正则表达式 $regex 。但是很多情境下这些手段不能够满足应用的需求。
下面这个范式介绍了一种在同一个文档内使用数组来保存关键词再对数组建多键索引 (multi-key index)的方式来实现关键词搜索。
为实现关键词搜索,在文档内增加一个数组字段并把每一个关键词加到数组里。然后你可以对该字段建一个多键索引。这样就可以对数组里面的关键词进行查询了。
假如你希望对图书馆的藏书实现一个按主题搜索的功能。 对每一本书,你可以加一个数组字段 topics 并把这本书相关的主题都加到这个数组里。
对于 Moby-Dick 这本书你可能会有以下这样的文档:
{ title : "Moby-Dick" ,
author : "Herman Melville" ,
published : 1851 ,
ISBN : 0451526996 ,
topics : [ "whaling" , "allegory" , "revenge" , "American" ,
"novel" , "nautical" , "voyage" , "Cape Cod" ]
}
然后对 topics 数组字段建多键索引:
db.volumes.createIndex( { topics: 1 } )
多键索引会对数组里的每一个值建立一个索引项。在这个例子里 whaling 和 allegory 个各有一个索引项。
现在你可以按关键词进行搜索,如:
db.volumes.findOne( { topics : "voyage" }, { title: 1 } )
注意
如果数组较大,达到几百或者几千以上的关键词,那么文档插入操作时的索引维护开支会大大增加。
关键词索引的局限:
对于MongoDB中的许多用例,相关数据存储在一个文档中的非规范化数据模型(Embedded Data Model)将是最佳的。 但是,在某些情况下,将相关信息存储在单独的文档中是有意义的,通常是在不同的集合或数据库中。
MongoDB 引用有两种:
_id
字段保存在另一个文档中作为参考。然后您的应用程序可以运行第二个查询以返回相关数据。对于大多数用例,这些引用很简单且足够。_id
字段,集合名称以及(可选)其数据库名称的值从一个文档到另一个文档的引用。通过包含这些名称,DBRefs允许位于多个集合中的文档更容易与来自单个集合的文档链接。除非一定要使用DBRefs,使用手动引用即可满足要求。
> original_id = ObjectId()
ObjectId("5c0c74fea1533dab560fd06b")
>db.sites.insert({"_id":original_id,"name":"baidu","url":"www.baidu.com"})
WriteResult({ "nInserted" : 1 })
> db.profile.insert({"name":"Rin", "site_id":original_id, "url":"www.baidu.com/Rin"})
WriteResult({ "nInserted" : 1 })
>
对于几乎所有要在两个文档之间存储关系的情况,请使用手动引用。 引用很容易创建,您的应用程序可以根据需要解析引用。
手动链接的唯一限制是这些引用不传达数据库和集合名称。 如果单个集合中的文档与多个集合中的文档相关,则可能需要考虑使用DBRefs。
DBRefs是表示文档的规范,而不是特定的引用类型。它们包括集合的名称,在某些情况下还包括数据库名称,以及_id字段中的值。
格式:
$ref
:$ref
字段包含引用文档所在的集合的名称。$id
: $id
字段包含引用文档中_id字段的值。$db
: 可选的,包含引用文档所在的数据库的名称。只有一些驱动程序支持$ db引用。> use test
switched to db test
> db.student.insert({"name":'Rin', "address":{"$ref":"sites", "$id":1, "$db":"first_demo"}})
WriteResult({ "nInserted" : 1 })
> var profile = db.profile.findOne({"name":"Tom"})
> var dbRef = profile.address
> db[dbRef.$ref].findOne({"_id":(dbRef.$id)})
{ "_id" : 2, "name" : "Tom", "num" : 1233 }