11.4 MongoDB 数据库基础
Express 的基本用法搞定之后,还有另一个绕不开的环节 —— 数据库操作。前端几乎不会与数据库打交道,因此本节需要细细阅读,着重理解。
但是大家也不必担心,我们使用的数据库是 MongoDB,我会用纯前端的方式给大家介绍如何使用数据库,就像在操作一个普通的 JSON 对象一样,保证大家可以理解。
在函数计算 FC 中可以使用其他云产品,本来计划买一个 MongoDB 版的云数据库,但看了一下发现太贵了,索性我就自己搭了一个。不过不管是云数据库还是自建数据库,使用方法都是一样的。
本章不会介绍如何搭建 MongoDB,只介绍其基础操作以及如何在 Express 中使用。MongoDB 的版本较多,下文的所有介绍和案例都是基于最新的 5.0 版本。
11.4.1 MongoDB 基本概念
提到数据库,可能大家听到最多的就是后端广泛应用的 MySQL。在公司经常听他们在聊设计表结构、写 SQL 语句什么的,因为 MySQL 是关系型数据库,数据操作需要用 SQL 语句,这对前端来说确实有一定的难度。
而 MongoDB 是一个文档型数据库,它没有像关系型数据库中的数据表、SQL 语句这些东西,它就是一个灵活的存储 JSON 数据的大仓库。对前端来说,我们操作数据的形式只是调用一些 MongoDB 内置方法,就像是使用 JavaScript 函数一样,因此我们完全可以从 JavaScript 的角度理解 MongoDB。
MongoDB 中有三个基本概念,分别是数据库、集合、文档。它们的解释如下:
数据库
(database):一个存储集合的仓库。MongoDB 允许创建多个数据库,数据库之间互相隔离。集合
(collection):一个数据库下有多个集合,可以把集合看作一个数组,存储一类数据。文档
(document):一个集合下存储多个文档,可以把文档看作一个普通对象,文档就是数据。
使用 JavaScript 模拟一下它们之间的关系:
var database = [];
var collection = [];
database.push(collection);
var doc1 = { id: 1, name: "小李" };
collection.push(doc1);
var doc2 = { id: 2, name: "小王" };
collection.push(doc2);
上述代码中,声明变量 database 相当于创建了数据库;变量 collection 是一个集合,被添加到数据库中;而 doc1、doc2 则是文档被添加到集合中,MongoDB 的存储结构大致就是如此。
文档是 MongoDB 中的数据,操作数据库就是在集合间检索和修改文档。下文会频繁提到文档,请记住它就是一个普通的 JSON 对象。
11.4.2 实现增查改删
假设现在有一个名为 testdb 的数据库,并且在该数据库中有一个存储日志的 logs 集合,我们来介绍一下如何在 logs 集合中实现数据的增查改删。
插入文档
MongoDB 使用 insertOne() 方法向集合中插入文档。假设现在要插入一条日志数据,方法如下:
testdb.logs.insertOne({
title: "登录操作",
content: "用户使用微信登录",
});
当然也支持批量插入,使用 insertMany() 方法,区别是参数是一个文档数组:
testdb.logs.insertMany([
{
title: "登录操作",
content: "用户使用微信登录",
},
]);
文档插入成功后,MongoDB 会为每个文档自动添加一个 _id 字段,字段值是使用 ObjectId() 方法生成的一个全局唯一的字符串,这样每个文档就有了唯一标识,方便后期检索。插入后的文档是这样的:
{
_id: '507f191e810c19729de860ea'
title: '登录操作',
content: '用户使用微信登录',
}
查询文档
查询文档主要使用 find() 方法,参数是一个对象,可以传入任意筛选条件。比如根据 _id 查到上一步添加的文档,方法如下:
testdb.logs.find({
_id: ObjectId("507f191e810c19729de860ea"),
});
注意,涉及到对 _id 字段的查询和修改必须使用 ObjectId() 方法包裹,直接使用字符串匹配不到。_id 是文档的唯一标识,下文我们用 ID 来表示当前集合的 _id 字段。
默认情况下查询条件都是等于操作。如果要执行非等于操作则需要使用操作符。比如我要查询集合中 title 字段包含 “登录” 两个字的文档,查询方法如下:
testdb.logs.find({
title: {
{ $regex: /登录/ }
},
})
上面代码中的 $regex 就是一个操作符,表示用正则表达式匹配 title 字段。MongoDB 提供了非常多的操作符,大家可以查询文档,常用的查询操作符如下:
$eq
:等于。$ne
:不等于。$gt
:大于。$gte
:大于或等于。$lt
:小于。$lte
:小于或等于。$not
:取反。$or
:或运算。$exists
:字段是否存在。$regex
:正则表达式。$size
:数组长度。
使用 find() 方法查询返回数组。如果你想查询符合条件的一个文档,请使用 findOne() 方法。
更新文档
更新文档主要使用 updateOne() 方法实现,它有两个参数,分别是过滤参数和更新操作符对象。假设我要根据 ID 找到某条文档并更新 content 字段,方法如下:
testdb.logs.updateOne(
{
_id: ObjectId("507f191e810c19729de860ea"),
},
{
$set: {
content: "用户使用支付宝登录",
},
}
);
上面代码中必须使用 $set 操作符来更新字段,不可以直接写要更新的字段。MongoDB 也提供了很多操作符用于更新文档,常见的更新操作符如下:
$set
:批量设置字段。$currentDate
:为字段设置当前时间。$unset
:批量删除字段。
更新文档也支持批量更新,只需要把 updateOne() 方法替换成 updateMany() 方法即可。因为它们接收的参数是一致的,只是更新逻辑不一致;前者只会更新匹配到的第一个文档,后者会更新匹配到的所有文档。
删除文档
删除文档的方法名是 deleteOne() 和 deleteMany(),分别表示删除一个文档和批量参数文档。两个方法都接受一个过滤参数,与查询文档的过滤规则一致。
下面代码删除所有 title 字段值不为空的文档:
testdb.logs.deleteMany({
title: { $ne: "" },
});
11.4.3 高级查询—聚合管道
使用 find() 方法可以在一个集合中查询数据。如果有更复杂的查询需求,比如多集合关联查询,以及一些数据处理筛选的操作,find() 方法就做不到了,此时需要 MongoDB 的高级查询功能 —— 聚合管道。
聚合管道顾名思义用于定义一批处理数据的管道,从第一个管道开始接收原始数据并做处理,然后将处理结果传给下一个管道,这样经过多个管道层层处理后返回最终的数据。
聚合管道使用 aggregate() 方法实现,它的参数是一个数组,每个数组项代表一条聚合管道,如下:
testdb.logs.aggregate([
{
$match: {
title: { $ne: "xxx" },
}, // 第一条管道:筛选数据
},
{
$project: {
title: 0,
count: { $size: "$arrs" },
}, // 第二条管道:修改字段
},
]);
每条聚合管道由一个管道阶段(Pipeline Stages)和多个管道操作符(Pipeline Operators)组成。管道阶段代表了本条管道要做什么,如代码中的 $match 和 $project;管道操作符决定具体怎么做,如代码中的 $ne 和 $size。
MongoDB 提供了许多高级的管道阶段可以应对各种各样的查询需求,常见的如下:
$match
:过滤筛选数据。$group
:分组查询。$project
:控制字段的显示隐藏。$addFields
:添加字段。$lookup
:集合关联查询。$sort
:列表排序。$limit
:限制返回条数。$facet
:重组数据格式。
还有更多用于操作数据的管道操作符,列举一小部分如下:
$filter
:数组过滤。$in
:检查值是否在数组内。$eq
:等于判断。$ne
:不等于判断。$split
:字符串切割。$replaceAll
:字符串替换。$toString
:转换字符串。$sum
:返回数值之和。
聚合管道的管道阶段和管道操作符单独讲不好理解,请先记住它们,后面我们在实际开发接口时用到哪个,再结合实际情况展开介绍。
在管道操作数据的过程中,为了快速访问到当前文档,我们需要一些变量。其中 “$$ROOT” 表示当前文档,“$_id” 表示文档的 _id 字段,“$name” 表示文档的 name 字段,以此类推。当设置自定义变量时,通过 “$$” 前缀加变量名访问。