Node.js: MongoDB 入门
最后更新:2026-08-26
1. 1 为什么选择 MongoDB
Alice 的电商 API 面临一个棘手问题:不同品类的产品属性差异巨大——笔记本电脑有 CPU 和内存字段,服装有尺码和颜色字段,食品有保质期字段。如果用关系型数据库,每次新增品类都要改表结构。MongoDB 的文档模型天然支持不同结构的记录,每个产品文档可以有完全不同的字段,无需任何 Schema 迁移。
- MongoDB 是文档型数据库,数据以 BSON 格式存储,类似 JSON 但支持更多类型
- 文档模型天然适配 Node.js 的对象模型,减少 ORM 转换开销
- 无固定 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 排序,skip 和 limit 实现分页。
▶ 示例:排序与分页
JAVASCRIPT
const page = 2;
const pageSize = 10;
const sorted = await products.find({})
.sort({ price: -1, name: 1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
.toArray();
1表示升序,-1表示降序- 多字段排序按声明顺序依次生效
skip过多时性能差,大数据量建议用范围查询替代
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 });
- MongoDB 自动为
_id创建唯一索引 - 复合索引遵循最左前缀原则
- 文本索引支持全文检索,每个集合仅一个
7. 7 MongoDB 连接与操作流程
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 练习
- 编写脚本连接本地 MongoDB,插入 5 条不同品类的产品文档,每条至少包含 3 个不同字段
- 实现按价格区间查询($gte/$lte)并按价格降序排序,仅返回 name 和 price 字段
- 使用
$in和$regex组合查询:category 在指定列表内且 name 包含某个关键词的产品 - 为常用查询字段创建复合索引,使用
explain()比较索引前后的执行计划差异 - 封装一个通用的分页查询函数,接受 filter/projection/sort/page/pageSize 参数
📖 小节
- 1 为什么选择 MongoDB的核心概念与使用方法
- 2 安装与连接的核心概念与使用方法
- 3 CRUD 操作的核心概念与使用方法
- 4 ObjectId 与查询操作符的核心概念与使用方法
- 5 投影与排序的核心概念与使用方法
- 6 索引基础的核心概念与使用方法
- 7 MongoDB 连接与操作流程的核心概念与使用方法
- 8 综合示例:产品管理数据访问层的核心概念与使用方法
📝 作业
- 完成本课所有代码示例,确保每个示例都能正确运行
- 修改综合示例,添加自己的扩展功能
- 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
- 思考:在实际项目中,你会如何应用本课学到的知识?
- 尝试将本课知识与前面课程的内容结合,构建一个小项目