MongoDB(三)数据模型

MongoDB(三)数据模型

数据建模的关键挑战是平衡应用程序的需求、数据库引擎的性能特征和数据检索模式,因此,数据模型也是一个应用程序好用与否的关键因素之一。

作者:lomtom

个人网站:lomtom.cn

个人公众号:博思奥园

你的支持就是我最大的动力。

MongoDB系列:

  1. MongoDB(一)初识MongoDB
  2. MongoDB(二)在Go中使用MongoDB原来这么简单
  3. MongoDB(三)数据模型
数据模型
数据建模的关键挑战是平衡应用程序的需求、数据库引擎的性能特征和数据检索模式,因此,数据模型也是一个应用程序好用与否的关键因素之一。

介绍

与一般的关系型数据不同,我们不必实现定义或声明我们的表结构(也就是建表),所以在使用上,我们不会被表的结构所拘束。这就很好的体现了MongoDB的灵活性。

恰恰因为这种灵活性,

  • 可以使得在单个集合中的文档不必具有相同的字段
  • 也可以使得在文档的单个嵌套字段不必具有相同的结构
  • 也同样在使用中有助于将MongoDB的文档映射到具体的实体当中。

模型设计的关键因素往往是围绕着怎么去表示数据之间的关系

MongoDB中,根据数据之间关系的表示,可以大致分为两种类型:嵌入式、引用式

引用式

有两种方法来实现引入式:

  1. Manual References
  2. 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: "杭州"
}
)

查询的话,有两种方法:

  1. 进行多次查询,需要将user的信息查出,然后对指定useridcontactaddress中查询.
  2. 使用聚合,官方提供了聚合函数,可以使用$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
// 1
{
"_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
// user
{
"id": 1111111,
"name": "lomtom"
}

// contact
{
"id": 222222,
"tel": "13512345678",
"mail": "lomtom@qq.com",
"user_id": 1111111
}

// address
{
"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. 多对多

那么,我们逐一进行击破。

一对一

上述就已经是一对一的实例了。

一对多

需求变动
例如,某个用户拥有联系方式(包括手机和邮箱)和地址(包括省份和城市),但是地址包括多个地址。
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
// user
{
"id": 1111111,
"name": "lomtom"
}

// contact
{
"id": 222222,
"tel": "13512345678",
"mail": "lomtom@qq.com",
"user_id": 1111111
}

// address
{
"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())时,每个文档的修改是原子的,但整个操作不是原子的。

小文档

  1. 如果一个集合包含大量小文档,出于性能原因,应该考虑嵌入。例如,可以通过逻辑功能对其进行分组。
  2. 对于集合中只包含很少的字段,可以考虑自定义_id(因为自动生成12 字节ObjectId)。或者可以采用较短的字段名称。

MongoDB(三)数据模型

https://lomtom.cn/98db8d1f.html

作者

lomtom

发布于

2021-12-21

更新于

2022-04-20

许可协议