数据建模的关键挑战是平衡应用程序的需求、数据库引擎的性能特征和数据检索模式,因此,数据模型也是一个应用程序好用与否的关键因素之一。
作者:lomtom
个人网站:lomtom.cn
个人公众号:博思奥园
你的支持就是我最大的动力。
MongoDB系列:
- MongoDB(一)初识MongoDB
- MongoDB(二)在Go中使用MongoDB原来这么简单
- MongoDB(三)数据模型
数据建模的关键挑战是平衡应用程序的需求、数据库引擎的性能特征和数据检索模式,因此,数据模型也是一个应用程序好用与否的关键因素之一。
介绍
与一般的关系型数据不同,我们不必实现定义或声明我们的表结构(也就是建表),所以在使用上,我们不会被表的结构所拘束。这就很好的体现了MongoDB
的灵活性。
恰恰因为这种灵活性,
- 可以使得在单个集合中的文档不必具有相同的字段
- 也可以使得在文档的单个嵌套字段不必具有相同的结构
- 也同样在使用中有助于将
MongoDB
的文档映射到具体的实体当中。
模型设计的关键因素往往是围绕着怎么去表示数据之间的关系。
在MongoDB
中,根据数据之间关系的表示,可以大致分为两种类型:嵌入式、引用式
引用式
有两种方法来实现引入式:
- Manual References
- DBRefs
Manual References
Manual References
将_id
(即主键)一个文档的字段保存 在另一个文档中作为依赖(理解为外键)。
Manual References
对于大多数情况来说都很简单且够用了。
例如,某个用户拥有联系方式(包括手机和邮箱)和地址(包括省份和城市)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| db.user.insertOne( { _id: "61c2840000bd3a593a5227c8", name: "lomtom" } )
db.contact.insertOne( { user_id: "61c2840000bd3a593a5227c8", tel: "13512345678", mail: "lomtom@qq.com" } )
db.address.insertOne( { user_id: "61c2840000bd3a593a5227c8", province: "浙江", city: "杭州" } )
|
查询的话,有两种方法:
- 进行多次查询,需要将
user
的信息查出,然后对指定user
的id
在contact
和address
中查询.
- 使用聚合,官方提供了聚合函数,可以使用
$lookup
进行查询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| db.user.aggregate([ { $lookup: { from: "contact", localField: "_id", foreignField: "user_id", as: "contact" } }, { $lookup: { from: "address", localField: "_id", foreignField: "user_id", as: "address" } } ])
|
最后结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| { "_id": "61c2840000bd3a593a5227c8", "name": "lomtom", "contact": [ { "_id": ObjectId("61c2842d00bd3a593a5227c9"), "user_id": "61c2840000bd3a593a5227c8", "tel": "13512345678", "mail": "lomtom@qq.com" } ], "address": [ { "_id": ObjectId("61c2843b00bd3a593a5227ca"), "user_id": "61c2840000bd3a593a5227c8", "province": "浙江", "city": "杭州" } ] }
|
但是,如果一个文档中对多个文档进行引用时,Manual References
显得不是那么适用了,这时候可以考虑使用BDRefs
。 (明明很适用)
DBRefs
DBRefs
使用一个文档的_id
字段、集合名称和可选的数据库名称的值来作为引用。也就是说我们使用DBRefs
来作为跨库的的引用,当然也可以不跨库进行引用(数据库为可选)。
格式
- $ref:表示要引用文档的
collection
名称
- $id:表示要引用文档的
_id
字段
- $db(可选):表示要引用的文档所在的
database
名称
根据之前的需求插入数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| db.user.insertOne( { _id: "61c2840000bd3a593a5227c8", name: "lomtom" } )
db.contact.insertOne( { user: { "$ref": "user", "$id": "61c2840000bd3a593a5227c8" }, tel: "13512345678", mail: "lomtom@qq.com" } )
db.address.insertOne( { user: { "$ref": "user", "$id": "61c2840000bd3a593a5227c8" }, province: "浙江", city: "杭州" } )
|
进行查询
1 2 3 4 5 6 7 8 9 10
| var contact = db.contact.findOne() var dbrefs = contact.user db[dbrefs.$ref].findOne({"_id":(dbrefs.$id)})
// 1 { "_id": "61c2840000bd3a593a5227c8", "name": "lomtom" }
|
虽然说有点麻烦,但是在一定程度上解决了一些问题,理论上同样是可以通过聚合进行查询。
值得注意的是,DBrefs
暂时只对一些语言进行了支持。
Driver |
DBRef Support |
Notes |
C |
Not Supported |
可以使用 Manual References |
C++ |
Not Supported |
可以使用 Manual References |
C# |
Supported |
请参考 C# driver page 查看更多信息 |
Go |
Not Supported |
可以使用 Manual References |
Haskell |
Not Supported |
可以使用 Manual References |
Java |
Supported |
请参考 Java driver page 查看更多信息 |
Node.js |
Supported |
请参考 Node.js driver page 查看更多信息 |
Perl |
Supported |
请参考 Perl driver page 查看更多信息 |
PHP |
Not Supported |
可以使用 Manual References |
Python |
Supported |
请参考 PyMongo driver page 查看更多信息 |
Ruby |
Supported |
请参考 Ruby driver page 查看更多信息 |
Scala |
Not Supported |
可以使用 Manual References |
嵌入式
引入
s嵌入式文档通过将相关数据存储在单个文档结构中来维护数据之间的关系。
嵌入式的结构模型可以允许我们将数据以嵌入的格式保存在与之关联的文档中。
例如,某个用户拥有联系方式(包括手机和邮箱)和地址(包括省份和城市)。
在关系型数据库中,为了更好的维护数据,我们会将联系方式、地址单独建表,并且与用户进行关联。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| { "id": 1111111, "name": "lomtom" }
{ "id": 222222, "tel": "13512345678", "mail": "lomtom@qq.com", "user_id": 1111111 }
{ "id": 333333, "province": "浙江", "city": "杭州" "user_id": 1111111 }
|
这样的处理方式看起来是没问题的,但是每次查询都需要关联三个表才能查到所有结果,并且插入或者更新,同样需要对三个表进行修改,这无疑会消耗过多的资源。
但是!!!如果采用MongoDB
,我们可以将联系方式和地址以嵌套的方式和用户信息保存在同一个文档当中。只需要简单的操作就可以满足常规的查询和写入操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| db.user.insertOne( { name: "lomtom", contact: { tel: "13512345678", mail: "lomtom@qq.com" }, address: { province: "浙江", city: "杭州" } } )
{ "_id": ObjectId("61c1a4e500bd3a593a5227c7"), "name": "lomtom", "contact": { "tel": "13512345678", "mail": "lomtom@qq.com" }, "address": { "province": "浙江", "city": "杭州" } }
|
那么你就会纳闷了,如果使用嵌入式,怎么满足基本的数据关系?
在常用的数据关系无外乎三种:
- 一对一
- 一对多
- 多对多
那么,我们逐一进行击破。
一对一
上述就已经是一对一的实例了。
一对多
例如,某个用户拥有联系方式(包括手机和邮箱)和地址(包括省份和城市),但是地址包括多个地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| { "id": 1111111, "name": "lomtom" }
{ "id": 222222, "tel": "13512345678", "mail": "lomtom@qq.com", "user_id": 1111111 }
{ "id": 333333, "province": "浙江", "city": "杭州" "user_id": 1111111 }
{ "id": 333334, "province": "浙江", "city": "杭州" "user_id": 1111111 }
|
改为嵌入式,可以在address
字段中采用数组的结构进行存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| db.user.insertOne( { name: "lomtom", contact: { tel: "13512345678", mail: "lomtom@qq.com" }, address: [{ province: "浙江", city: "杭州" }, { province: "湖南", city: "长沙" }] } )
// 1 { "_id": ObjectId("61c2940400bd3a593a5227ce"), "name": "lomtom", "contact": { "tel": "13512345678", "mail": "lomtom@qq.com" }, "address": [ { "province": "浙江", "city": "杭州" }, { "province": "湖南", "city": "长沙" } ] }
|
多对多
单个嵌入式是没办法满足多对多的(会有过多的数据冗余),需要结合引用来使用。
注意
原子性
在MongoDB中,写操作是对单个文件的能级,即使操作修改多个嵌入式文档 内的单个文件。当单个写操作修改多个文档(例如db.collection.updateMany()
)时,每个文档的修改是原子的,但整个操作不是原子的。
小文档
- 如果一个集合包含大量小文档,出于性能原因,应该考虑嵌入。例如,可以通过逻辑功能对其进行分组。
- 对于集合中只包含很少的字段,可以考虑自定义
_id
(因为自动生成12 字节ObjectId)。或者可以采用较短的字段名称。