人人都会AI编程

13.2 NoSQL 数据库

更新时间:2026-07-11

在 Node.js 生态中,除了 MySQL、PostgreSQL 等关系型数据库,NoSQL 数据库同样占据着重要位置。它们以灵活的数据模型和高扩展性著称,特别适合数据结构多变、需要快速迭代的项目。MongoDB 是其中最具代表性的一种,官方驱动的 Node.js 支持度极高,而 Mongoose 则进一步提供了声明式的 Schema 定义、数据校验和查询抽象。本节将聚焦 MongoDB 的核心用法,用真实的项目视角串联从连接、建模到聚合查询的完整链路。

13.2.1 MongoDB 的文档模型与设计哲学

MongoDB 是一个面向文档的数据库,它不再使用表、行和列,而是将数据存储为类似 JSON 格式的 BSON 文档。一个集合(collection)就可以看成是一堆文档的容器,而同一集合内的文档结构可以完全不同。不过在实际应用中,我们通常会通过应用层(如 Mongoose)来约束结构,以保证数据的可维护性。

文档模型最突出的优势是数据内嵌:你可以把关联数据直接嵌入到父文档中,而不必像关系型数据库那样总是执行 JOIN。例如,一个用户文档可以直接内嵌多个地址对象:

{
  "_id": ObjectId("..."),
  "name": "张三",
  "email": "zhangsan@example.com",
  "addresses": [
    { "type": "home", "street": "中山路1号", "city": "北京" },
    { "type": "work", "street": "科技园路100号", "city": "上海" }
  ]
}

这不仅简化了读取操作(一次查询就能拿到用户及其所有地址),也天然适配现代前端对聚合数据的需求。当然,内嵌也带来了更新一致性和存储膨胀的潜在问题,需要在设计时就仔细权衡。

13.2.2 Mongoose:优雅地定义 Schema 与数据校验

官方 MongoDB Node.js 驱动提供了基础操作能力,但直接使用时会面临这些问题:缺乏结构约束、无内建数据校验、回调或 Promise 链写起来冗长。而 Mongoose 作为应用最广的 ODM(Object Document Mapper),把数据建模、校验、业务逻辑都封装进了 Schema 和 Model 体系。

1. 安装与连接

npm install mongoose

连接 MongoDB 比较简单,推荐在连接字符串中设置数据库名和连接池大小:

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/shop', {
  maxPoolSize: 10,         // 连接池大小
  serverSelectionTimeoutMS: 5000, // 超时
}).then(() => console.log('MongoDB 连接成功'))
  .catch(err => console.error('连接失败', err));

2. 定义 Schema 与 Model

Schema 是 Mongoose 的核心,它描述文档的结构、字段类型、默认值、校验规则和索引。定义好 Schema 后,用 mongoose.model() 转换为可以增删改查的 Model。

const { Schema, model } = mongoose;

const productSchema = new Schema({
  name: {
    type: String,
    required: [true, '商品名不能为空'],
    trim: true,
    maxlength: [100, '商品名最多100字符']
  },
  price: {
    type: Number,
    required: true,
    min: [0.01, '价格必须大于0']
  },
  tags: [String],  // 字符串数组
  stock: {
    type: Number,
    default: 0
  },
  category: {
    type: Schema.Types.ObjectId,
    ref: 'Category'  // 关联 Category 集合
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 创建索引(通常在 Schema 级别定义更优)
productSchema.index({ name: 1, category: 1 });
productSchema.index({ tags: 1 });

const Product = model('Product', productSchema);

3. 内建校验与自定义逻辑

Mongoose 内置了丰富的校验选项(required、min、max、enum、match 等),你也可以添加自定义校验函数。此外,利用 pre / post 钩子可以在保存前后执行逻辑,比如自动更新时间或密码哈希。

userSchema.pre('save', async function(next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});

13.2.3 增删改查与复杂查询

Mongoose Model 提供了简洁的 API 完成 CRUD,同时支持原生 MongoDB 查询语法。以下是一些典型场景:

创建文档

const product = new Product({
  name: '蓝牙耳机',
  price: 299.99,
  tags: ['电子', '音频'],
  stock: 100
});
await product.save();

或用 create 一步完成:

await Product.create({ name: '鼠标', price: 79, stock: 200 });

查询文档

查询支持链式调用,条件、投影、排序、分页一气呵成:

// 查找所有价格在 50 到 500 之间的商品,按价格排序,分页
const products = await Product.find({
  price: { $gte: 50, $lte: 500 },
  stock: { $gt: 0 }
})
  .select('name price stock')   // 投影
  .sort({ price: -1 })
  .skip(0)
  .limit(20);

常用查询操作符:$gt$lt$in$nin$exists$regex 等,几乎可以表达任意业务逻辑。对于需要引用其他集合的字段,Mongoose 提供 populate 来“连接”数据:

const product = await Product.findById(productId).populate('category', 'name');
console.log(product.category.name);

更新文档

可以使用 updateOnefindByIdAndUpdate 等方法:

await Product.updateOne(
  { name: '蓝牙耳机' },
  { $set: { price: 259.99 }, $inc: { stock: -1 } }
);

$set$inc 等原子操作避免了并发更新时的数据不一致。

删除文档

await Product.deleteOne({ _id: productId });
// 或 findByIdAndDelete

13.2.4 聚合查询:强大的数据处理管道

MongoDB 的聚合框架(Aggregation Pipeline)可以在数据库内部完成复杂的数据转换、分组、排序、连接,避免将大量数据拉到应用层再处理。对于统计、报表等业务,聚合查询几乎是标配。

一条聚合管道由多个阶段组成,每个阶段处理上层输出并传给下一阶段。常用阶段:$match(过滤)、$group(分组计算)、$sort(排序)、$project(投影)、$unwind(展开数组)、$lookup(关联查询)等。

例如,统计每个分类下商品的均价和总数:

const result = await Product.aggregate([
  { $match: { stock: { $gt: 0 } } },
  { $group: {
      _id: '$category',
      avgPrice: { $avg: '$price' },
      total: { $sum: 1 }
  }},
  { $sort: { avgPrice: -1 } },
  { $lookup: {
      from: 'categories',
      localField: '_id',
      foreignField: '_id',
      as: 'category'
  }},
  { $unwind: '$category' },
  { $project: {
      name: '$category.name',
      avgPrice: 1,
      total: 1
  }}
]);

这段管道依次完成:过滤有库存的商品 → 按分类分组计算均价和数量 → 降序排列 → 关联分类集合获取名称 → 展开数组 → 输出所需的字段。整个复杂逻辑一次往返数据库即可完成,效率远高于多次查询并在 Node.js 中手工合并。

13.2.5 索引与性能优化

MongoDB 的索引机制和关系型数据库类似,都是为了加速查询。没有索引的情况下,查询会进行全集合扫描,在数据量稍大时性能急剧下降。在 Mongoose 中,推荐在 Schema 级别定义索引:

productSchema.index({ price: 1 });              // 单字段索引
productSchema.index({ category: 1, price: -1 }); // 复合索引
productSchema.index({ name: 'text' });          // 文本索引,用于全文搜索

在生产环境,使用 explain() 分析方法可以查看查询到底使用了哪个索引、扫描了多少文档。

此外,几个实用优化技巧:

  • 覆丶盖查询:使用 select() 只返回需要的字段,并确保这些字段均已索引,可实现“仅索引扫描”,无需读取文档数据。
  • 控制文档大小:内嵌数组不要无限增长,例如用户购物车可用单独集合存储,而不是直接塞进用户文档。
  • 避免耗时的跳过:使用范围查询配合索引替代大偏移量的 skip,或者使用基于时间戳的流式分页。
  • 慢查询监控:MongoDB 提供 Profiler,可记录执行时间超过阈值的操作,再通过 explain 分析优化。

13.2.6 Mongoose 与原生驱动的选择

虽然 Mongoose 抽象了很多便利功能,但并非所有项目都必须使用它。在某些轻量场景或 Serverless 中,直接使用 MongoDB 原生驱动可能更灵活,也减少了依赖体积。对于需要强 Schema 校验、中间件逻辑、关联引用的复杂业务,Mongoose 提供的结构性价值则非常突出。团队可根据项目规模自行权衡。

13.2.7 与关系型数据库的互补

我们已经讨论过 MySQL 和 Sequelize/TypeORM,那么什么时候应该选择 MongoDB 呢?

  • 数据模型经常变动:创业初期或需求频繁调整时,无需 ALTER TABLE 的特性可以大幅提升迭代速度。
  • 明显的嵌入关系:如内容管理中的文章与评论、用户与地址等,内嵌读取非常高效。
  • 高写入吞吐:MongoDB 的写入性能通常优于传统关系型数据库,适合日志、埋点等场景。

但关系型数据库的事务、JOIN 能力依然是强需求时的优势。现代 MongoDB 已支持多文档事务,但开销较大,不建议滥用。如果业务需要频繁的跨集合关联和强一致性,可能应该回头考虑关系型方案。很多大型项目甚至混合使用两者,各取所长。


以上就是 Node.js 中 NoSQL 数据库环节的核心实践。拥有 Mongoose 和 MongoDB 的组合,你的全栈工具箱中就多了一种极其灵活的数据存储方案。在后续章节中,我们将进入缓存与中间件领域,探讨 Redis 如何为 Node.js 应用注入性能加速剂。