Skip to content

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 模拟一下它们之间的关系:

js
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() 方法向集合中插入文档。假设现在要插入一条日志数据,方法如下:

js
testdb.logs.insertOne({
  title: "登录操作",
  content: "用户使用微信登录",
});

当然也支持批量插入,使用 insertMany() 方法,区别是参数是一个文档数组:

js
testdb.logs.insertMany([
  {
    title: "登录操作",
    content: "用户使用微信登录",
  },
]);

文档插入成功后,MongoDB 会为每个文档自动添加一个 _id 字段,字段值是使用 ObjectId() 方法生成的一个全局唯一的字符串,这样每个文档就有了唯一标识,方便后期检索。插入后的文档是这样的:

js
{
  _id: '507f191e810c19729de860ea'
  title: '登录操作',
  content: '用户使用微信登录',
}

查询文档

查询文档主要使用 find() 方法,参数是一个对象,可以传入任意筛选条件。比如根据 _id 查到上一步添加的文档,方法如下:

js
testdb.logs.find({
  _id: ObjectId("507f191e810c19729de860ea"),
});

注意,涉及到对 _id 字段的查询和修改必须使用 ObjectId() 方法包裹,直接使用字符串匹配不到。_id 是文档的唯一标识,下文我们用 ID 来表示当前集合的 _id 字段。

默认情况下查询条件都是等于操作。如果要执行非等于操作则需要使用操作符。比如我要查询集合中 title 字段包含 “登录” 两个字的文档,查询方法如下:

js
testdb.logs.find({
  title: {
    { $regex: /登录/ }
  },
})

上面代码中的 $regex 就是一个操作符,表示用正则表达式匹配 title 字段。MongoDB 提供了非常多的操作符,大家可以查询文档,常用的查询操作符如下:

  • $eq:等于。
  • $ne:不等于。
  • $gt:大于。
  • $gte:大于或等于。
  • $lt:小于。
  • $lte:小于或等于。
  • $not:取反。
  • $or:或运算。
  • $exists:字段是否存在。
  • $regex:正则表达式。
  • $size:数组长度。

使用 find() 方法查询返回数组。如果你想查询符合条件的一个文档,请使用 findOne() 方法。

更新文档

更新文档主要使用 updateOne() 方法实现,它有两个参数,分别是过滤参数和更新操作符对象。假设我要根据 ID 找到某条文档并更新 content 字段,方法如下:

js
testdb.logs.updateOne(
  {
    _id: ObjectId("507f191e810c19729de860ea"),
  },
  {
    $set: {
      content: "用户使用支付宝登录",
    },
  }
);

上面代码中必须使用 $set 操作符来更新字段,不可以直接写要更新的字段。MongoDB 也提供了很多操作符用于更新文档,常见的更新操作符如下:

  • $set:批量设置字段。
  • $currentDate:为字段设置当前时间。
  • $unset:批量删除字段。

更新文档也支持批量更新,只需要把 updateOne() 方法替换成 updateMany() 方法即可。因为它们接收的参数是一致的,只是更新逻辑不一致;前者只会更新匹配到的第一个文档,后者会更新匹配到的所有文档。

删除文档

删除文档的方法名是 deleteOne() 和 deleteMany(),分别表示删除一个文档和批量参数文档。两个方法都接受一个过滤参数,与查询文档的过滤规则一致。

下面代码删除所有 title 字段值不为空的文档:

js
testdb.logs.deleteMany({
  title: { $ne: "" },
});

11.4.3 高级查询—聚合管道

使用 find() 方法可以在一个集合中查询数据。如果有更复杂的查询需求,比如多集合关联查询,以及一些数据处理筛选的操作,find() 方法就做不到了,此时需要 MongoDB 的高级查询功能 —— 聚合管道。

聚合管道顾名思义用于定义一批处理数据的管道,从第一个管道开始接收原始数据并做处理,然后将处理结果传给下一个管道,这样经过多个管道层层处理后返回最终的数据。

聚合管道使用 aggregate() 方法实现,它的参数是一个数组,每个数组项代表一条聚合管道,如下:

js
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 字段,以此类推。当设置自定义变量时,通过 “$$” 前缀加变量名访问。