Node.js: MongoDB 入门

最后更新:2026-08-26

1. 1 为什么选择 MongoDB

Alice 的电商 API 面临一个棘手问题:不同品类的产品属性差异巨大——笔记本电脑有 CPU 和内存字段,服装有尺码和颜色字段,食品有保质期字段。如果用关系型数据库,每次新增品类都要改表结构。MongoDB 的文档模型天然支持不同结构的记录,每个产品文档可以有完全不同的字段,无需任何 Schema 迁移。

SQL 概念 MongoDB 概念 说明
Database Database 数据库,概念一致
Table Collection 表→集合
Row Document 行→文档
Column Field 列→字段
Primary Key _id (ObjectId) 主键自动生成
JOIN $lookup (聚合) 关联查询方式不同
Schema 无强制 Schema 可选 Validation Rule
Index Index 索引机制相似


2. 2 安装与连接

(1) 安装 MongoDB 驱动

使用官方 mongodb npm 包连接 MongoDB 服务器。

▶ 示例:安装驱动

BASH
npm install mongodb

(2) 创建客户端连接

MongoClient 是连接 MongoDB 的入口,通过连接字符串指定地址和选项。

▶ 示例:基本连接

JAVASCRIPT
const { MongoClient } = require('mongodb');

const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);

async function connect() {
  try {
    await client.connect();
    console.log('Connected to MongoDB');
    const db = client.db('myapp');
    return db;
  } catch (err) {
    console.error('Connection failed:', err.message);
    process.exit(1);
  }
}
▶ 试一试

(3) 连接池与配置选项

MongoClient 内置连接池,通过选项控制池大小和行为。

▶ 示例:带连接池配置的连接

JAVASCRIPT
const client = new MongoClient(uri, {
  maxPoolSize: 10,
  minPoolSize: 2,
  maxIdleTimeMS: 30000,
  serverSelectionTimeoutMS: 5000,
  connectTimeoutMS: 10000,
});
▶ 试一试
配置选项 默认值 说明
maxPoolSize 100 连接池最大连接数
minPoolSize 0 连接池最小连接数
maxIdleTimeMS 0 空闲连接超时(0=不超时)
serverSelectionTimeoutMS 30000 服务器选择超时
connectTimeoutMS 30000 连接建立超时
socketTimeoutMS 0 Socket 超时
retryWrites true 自动重试写入操作

(4) 优雅关闭连接

应用退出时必须关闭连接,释放资源。

▶ 示例:优雅关闭

JAVASCRIPT
process.on('SIGINT', async () => {
  await client.close();
  console.log('MongoDB connection closed');
  process.exit(0);
});
▶ 试一试

3. 3 CRUD 操作

(1) 插入文档

使用 insertOne 插入单条文档,insertMany 批量插入。

▶ 示例:插入产品文档

JAVASCRIPT
const products = db.collection('products');

const result = await products.insertOne({
  name: 'Mechanical Keyboard',
  price: 89.99,
  category: 'electronics',
  specs: { switches: 'Cherry MX Blue', layout: 'ANSI' },
  createdAt: new Date(),
});

console.log('Inserted ID:', result.insertedId);
▶ 试一试

(2) 查询文档

find 返回游标,findOne 返回单条文档。

▶ 示例:查询产品

JAVASCRIPT
const product = await products.findOne({ category: 'electronics' });
console.log(product);

const cursor = products.find({ price: { $gt: 50 } });
const expensive = await cursor.toArray();
console.log(`${expensive.length} products found`);
▶ 试一试

(3) 更新文档

updateOne 更新匹配的第一条,updateMany 更新所有匹配。

▶ 示例:更新产品价格

JAVASCRIPT
const updateResult = await products.updateOne(
  { name: 'Mechanical Keyboard' },
  { $set: { price: 79.99, updatedAt: new Date() } },
);

console.log('Modified count:', updateResult.modifiedCount);
▶ 试一试

(4) 删除文档

deleteOne 删除匹配的第一条,deleteMany 删除所有匹配。

▶ 示例:删除产品

JAVASCRIPT
const deleteResult = await products.deleteOne({
  name: 'Mechanical Keyboard',
});

console.log('Deleted count:', deleteResult.deletedCount);
▶ 试一试

(5) CRUD 方法速查

操作 方法 返回值 说明
插入单条 insertOne(doc) {insertedId} 返回自动生成的 ID
插入多条 insertMany([doc]) {insertedIds, insertedCount} 批量插入
查询单条 findOne(filter) Document or null 返回第一条匹配
查询多条 find(filter) Cursor toArray() 或遍历
更新单条 updateOne(filter, update) {modifiedCount} 只更新第一条
更新多条 updateMany(filter, update) {modifiedCount} 更新所有匹配
删除单条 deleteOne(filter) {deletedCount} 删除第一条匹配
删除多条 deleteMany(filter) {deletedCount} 删除所有匹配
替换文档 replaceOne(filter, doc) {modifiedCount} 整文档替换


4. 4 ObjectId 与查询操作符

(1) ObjectId 机制

每个文档的 _id 默认是 ObjectId 类型,12 字节编码包含时间戳、机器标识和计数器。

▶ 示例:ObjectId 的使用

JAVASCRIPT
const { ObjectId } = require('mongodb');

const id = new ObjectId();
console.log('ID string:', id.toHexString());
console.log('Timestamp:', id.getTimestamp());

const product = await products.findOne({
  _id: new ObjectId('6850a1b2c3d4e5f6a7b8c9d0'),
});
▶ 试一试

(2) 比较操作符

▶ 示例:比较查询

JAVASCRIPT
const expensive = await products.find({ price: { $gt: 100 } }).toArray();
const cheap = await products.find({ price: { $lt: 20 } }).toArray();
const midRange = await products.find({ price: { $gte: 50, $lte: 100 } }).toArray();
▶ 试一试

(3) 逻辑与集合操作符

▶ 示例:$in 和 $or 查询

JAVASCRIPT
const selected = await products.find({
  category: { $in: ['electronics', 'books'] },
}).toArray();

const mixed = await products.find({
  $or: [
    { price: { $lt: 10 } },
    { category: 'electronics' },
  ],
}).toArray();
▶ 试一试

(4) 正则查询

▶ 示例:正则匹配产品名

JAVASCRIPT
const matched = await products.find({
  name: { $regex: /^Mechanical/i },
}).toArray();
▶ 试一试

(5) 查询操作符速查

操作符 语法 说明
$eq {field: {$eq: val}} 等于(同 {field: val}
$gt {field: {$gt: val}} 大于
$gte {field: {$gte: val}} 大于等于
$lt {field: {$lt: val}} 小于
$lte {field: {$lte: val}} 小于等于
$ne {field: {$ne: val}} 不等于
$in {field: {$in: [v1,v2]}} 在数组内
$nin {field: {$nin: [v1,v2]}} 不在数组内
$or {$or: [{...},{...}]} 或条件
$and {$and: [{...},{...}]} 与条件
$not {field: {$not: {...}}} 取反
$regex {field: {$regex: 'pattern'}} 正则匹配
$exists {field: {$exists: true}} 字段是否存在


5. 5 投影与排序

(1) 投影控制返回字段

投影指定返回或排除哪些字段,减少网络传输。

▶ 示例:投影查询

JAVASCRIPT
const names = await products.find(
  {},
  { projection: { name: 1, price: 1, _id: 0 } },
).toArray();

const withoutSpecs = await products.find(
  {},
  { projection: { specs: 0, createdAt: 0 } },
).toArray();
▶ 试一试

(2) 排序与分页

sort 排序,skiplimit 实现分页。

▶ 示例:排序与分页

JAVASCRIPT
const page = 2;
const pageSize = 10;

const sorted = await products.find({})
  .sort({ price: -1, name: 1 })
  .skip((page - 1) * pageSize)
  .limit(pageSize)
  .toArray();
▶ 试一试

6. 6 索引基础

(1) 创建索引

索引加速查询,但增加写入开销和存储空间。

▶ 示例:创建索引

JAVASCRIPT
await products.createIndex({ name: 1 });
await products.createIndex({ category: 1, price: -1 });
await products.createIndex({ name: 'text' });

const indexes = await products.indexes();
console.log(indexes);
▶ 试一试

(2) 唯一索引与复合索引

▶ 示例:唯一索引

JAVASCRIPT
await products.createIndex({ sku: 1 }, { unique: true });
▶ 试一试

7. 7 MongoDB 连接与操作流程

100%
flowchart TD
    A[应用启动] --> B[创建 MongoClient]
    B --> C[client.connect]
    C -->|成功| D[获取 db 实例]
    C -->|失败| E[错误处理/重试]
    E --> C
    D --> F[获取 collection]
    F --> G{CRUD 操作}
    G -->|写入| H[insertOne / insertMany]
    G -->|读取| I[find / findOne]
    G -->|更新| J[updateOne / updateMany]
    G -->|删除| K[deleteOne / deleteMany]
    H --> L[返回 insertedId]
    I --> M[返回文档/游标]
    J --> N[返回 modifiedCount]
    K --> O[返回 deletedCount]
    L --> P{继续操作?}
    M --> P
    N --> P
    O --> P
    P -->|是| G
    P -->|否| Q[client.close]
    Q --> R[应用退出]


8. 8 综合示例:产品管理数据访问层

JAVASCRIPT
const { MongoClient, ObjectId } = require('mongodb');

class ProductRepository {
  constructor(uri, dbName) {
    this.client = new MongoClient(uri, {
      maxPoolSize: 10,
      serverSelectionTimeoutMS: 5000,
    });
    this.dbName = dbName;
    this.collection = null;
  }

  async connect() {
    await this.client.connect();
    const db = this.client.db(this.dbName);
    this.collection = db.collection('products');
    await this.collection.createIndex({ name: 1 });
    await this.collection.createIndex({ category: 1, price: -1 });
    console.log('ProductRepository connected');
  }

  async create(productData) {
    const doc = {
      ...productData,
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    const result = await this.collection.insertOne(doc);
    return { ...doc, _id: result.insertedId };
  }

  async findById(id) {
    return await this.collection.findOne({
      _id: new ObjectId(id),
    });
  }

  async findByCategory(category, page = 1, pageSize = 10) {
    const skip = (page - 1) * pageSize;
    const [items, total] = await Promise.all([
      this.collection.find({ category })
        .sort({ price: -1 })
        .skip(skip)
        .limit(pageSize)
        .project({ name: 1, price: 1, category: 1 })
        .toArray(),
      this.collection.countDocuments({ category }),
    ]);
    return { items, total, page, pageSize };
  }

  async update(id, updates) {
    const result = await this.collection.updateOne(
      { _id: new ObjectId(id) },
      { $set: { ...updates, updatedAt: new Date() } },
    );
    return result.modifiedCount > 0;
  }

  async delete(id) {
    const result = await this.collection.deleteOne({
      _id: new ObjectId(id),
    });
    return result.deletedCount > 0;
  }

  async disconnect() {
    await this.client.close();
    console.log('ProductRepository disconnected');
  }
}

async function main() {
  const repo = new ProductRepository(
    'mongodb://localhost:27017',
    'ecommerce',
  );
  try {
    await repo.connect();
    const created = await repo.create({
      name: 'Wireless Mouse',
      price: 29.99,
      category: 'electronics',
      specs: { dpi: 16000, buttons: 6 },
    });
    console.log('Created:', created._id);
    const found = await repo.findById(created._id);
    console.log('Found:', found.name);
    await repo.update(created._id, { price: 24.99 });
    const page = await repo.findByCategory('electronics', 1, 10);
    console.log('Page items:', page.items.length);
    await repo.delete(created._id);
    console.log('Deleted');
  } finally {
    await repo.disconnect();
  }
}

main().catch(console.error);

❓ 常见问题

Q 为什么用 MongoDB 不用 MySQL?
A 当数据结构频繁变化、字段不固定时,MongoDB 的文档模型无需改表即可适应;如果业务涉及大量事务和复杂关联,MySQL 更合适。
Q ObjectId 是什么?
A ObjectId 是 MongoDB 自动生成的 12 字节唯一标识,前 4 字节是时间戳,可通过 id.getTimestamp() 获取创建时间,无需额外字段。
Q 连接池大小怎么设?
A 一般设为 CPU 核心数的 5-10 倍(maxPoolSize),I/O 密集型可适当增大;minPoolSize 设为 2-5 避免冷启动延迟。
Q 如何处理连接失败?
A 设置 serverSelectionTimeoutMS 限制等待时间,在 catch 中记录日志并优雅降级;生产环境建议配合重试机制和健康检查。
Q MongoDB 适合什么场景?
A 适合内容管理、日志分析、IoT 数据、商品目录等 Schema 灵活、读写频繁的场景;不适合强事务一致性要求的金融核心系统。
Q 投影中 1 和 0 能混用吗?
A_id 外不能混用——要么全部用 1 包含指定字段,要么全部用 0 排除指定字段;_id 默认返回,可单独设为 0 排除。


9. 10 练习

  1. 编写脚本连接本地 MongoDB,插入 5 条不同品类的产品文档,每条至少包含 3 个不同字段
  2. 实现按价格区间查询($gte/$lte)并按价格降序排序,仅返回 name 和 price 字段
  3. 使用 $in$regex 组合查询:category 在指定列表内且 name 包含某个关键词的产品
  4. 为常用查询字段创建复合索引,使用 explain() 比较索引前后的执行计划差异
  5. 封装一个通用的分页查询函数,接受 filter/projection/sort/page/pageSize 参数

📖 小节


📝 作业

  1. 完成本课所有代码示例,确保每个示例都能正确运行
  2. 修改综合示例,添加自己的扩展功能
  3. 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
  4. 思考:在实际项目中,你会如何应用本课学到的知识?
  5. 尝试将本课知识与前面课程的内容结合,构建一个小项目
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏