想象一下,你已经安装好了 MongoDB,也理解了文档、集合、BSON 等核心概念,现在你面临的问题是:如何在 MongoDB 中创建、读取、更新和删除数据?
CRUD 操作是数据库应用的基石,也是开发者每天都要面对的任务。本文将带你系统掌握 MongoDB 的四大基础操作,通过丰富的实例和最佳实践,让你能够自信地进行数据操作。
今天,我们将从最简单的插入一条数据开始,逐步深入到批量操作、条件查询、复杂更新,最后掌握完整的 CRUD 操作体系。
目录
- 为什么 CRUD 操作如此重要?
- 准备工作:连接 MongoDB
- Create - 创建操作:插入数据
- Read - 读取操作:查询数据
- Update - 更新操作:修改数据
- Delete - 删除操作:移除数据
- 批量操作:bulkWrite()
- 实际应用场景
- 企业开发中的常见问题与解决方案
- 企业级 CRUD 操作解决方案
- 总结:从基础到企业级的完整闭环
- 互动与下一步
为什么 CRUD 操作如此重要?
CRUD 是数据库操作的核心
CRUD 代表四种基本的数据操作:
- C (Create) - 创建/插入数据
- R (Read) - 读取/查询数据
- U (Update) - 更新/修改数据
- D (Delete) - 删除数据
MongoDB vs 传统 SQL 对比
让我们先看看 MongoDB 和 SQL 在 CRUD 操作上的对应关系:
| 操作 | SQL | MongoDB | 说明 |
|---|---|---|---|
| 创建 | INSERT INTO | insertOne() / insertMany() | 插入文档 |
| 读取 | SELECT | find() / findOne() | 查询文档 |
| 更新 | UPDATE | updateOne() / updateMany() | 更新文档 |
| 删除 | DELETE | deleteOne() / deleteMany() | 删除文档 |
| 替换 | REPLACE (不常用) | replaceOne() | 完整替换文档 |
| 查找并 | - | findOneAndUpdate() | 原子性查找并更新 |
| 批量 | INSERT … SELECT | bulkWrite() | 批量操作 |
| 计数 | SELECT COUNT(*) | countDocuments() | 统计文档数量 |
| 去重 | SELECT DISTINCT | distinct() | 获取唯一值 |
| 聚合 | GROUP BY, JOIN, HAVING | aggregate() | 复杂数据分析 |
MongoDB 的优势:
- 方法名更直观易懂
- 支持灵活的 JSON 格式
- 单个方法完成复杂操作
- 原子性操作更强大
准备工作:连接 MongoDB
在开始 CRUD 操作之前,我们需要连接到 MongoDB 数据库:
使用 MongoDB Shell (mongosh)
MongoDB Shell 的本质:
MongoDB Shell (mongosh) 本质上是一个基于 Node.js 的交互式 JavaScript 运行环境,这意味着:
- 可以直接执行 JavaScript 代码
- 支持 ES6+ 语法(let, const, 箭头函数等)
- 可以加载和执行外部 JavaScript 文件
- 内置了 MongoDB 驱动和辅助函数
基础连接命令:
# 启动 MongoDB Shell
mongosh
# 指定连接字符串
mongosh "mongodb://localhost:27017"
# 连接到指定数据库
mongosh "mongodb://localhost:27017/blogDB"
# 带认证连接
mongosh "mongodb://username:password@localhost:27017/blogDB"
Shell 中的基础命令:
// MongoDB Shell 命令
use blogDB // 选择或创建数据库
db // 查看当前数据库
show dbs // 查看所有数据库
show collections // 查看当前数据库的所有集合
扩展:加载外部 JavaScript 文件
为什么 MongoDB Shell 能加载 JS 文件?
因为 mongosh 本质就是一个 Node.js 程序!既然是 JavaScript 环境,自然能运行 .js 文件。
实际用处举例:
想象你要给 100 个测试账号批量初始化数据,难道要在命令行里敲 100 遍 insertOne?显然不现实。写成脚本文件,一条命令搞定:
mongosh --file initTestUsers.js
MongoDB Shell 支持加载和执行外部 JavaScript 文件:
常用脚本示例:
示例 1:批量初始化测试数据
// 文件:initTestData.js
use blogDB;
// 批量创建 100 个测试用户
const users = [];
for (let i = 1; i <= 100; i++) {
users.push({
username: `testuser${i}`,
email: `test${i}@example.com`,
age: 20 + (i % 30),
createdAt: new Date()
});
}
db.users.insertMany(users);
print(`创建了 ${users.length} 个测试用户`);
# 执行脚本
mongosh --file initTestData.js
示例 2:定时清理过期数据
// 文件:cleanup.js
// 每天定时执行,清理 30 天前的日志
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const result = db.logs.deleteMany({ createdAt: { $lt: thirtyDaysAgo } });
print(`清理了 ${result.deletedCount} 条过期日志`);
示例 3:在 Shell 中加载工具函数
// 文件:utils.js
function getStats() {
return {
users: db.users.countDocuments(),
articles: db.articles.countDocuments(),
};
}
// 在 mongosh 中使用
load("utils.js");
getStats(); // 直接调用
核心优势:一次编写,重复使用
- 避免重复敲命令
- 脚本可以版本控制(Git)
- 适合自动化和 CI/CD
使用 Node.js 驱动
// Node.js 驱动连接示例
const { MongoClient } = require("mongodb");
// 连接字符串
const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri);
async function main() {
try {
// 连接到 MongoDB
await client.connect();
console.log("成功连接到 MongoDB");
// 选择数据库和集合
const database = client.db("blogDB");
const collection = database.collection("users");
// 在这里执行 CRUD 操作
} finally {
// 关闭连接
await client.close();
}
}
main().catch(console.error);
使用 Python 驱动 (PyMongo)
# Python PyMongo 驱动连接示例
from pymongo import MongoClient
# 连接到 MongoDB
client = MongoClient('mongodb://localhost:27017/')
# 选择数据库和集合
db = client['blogDB']
collection = db['users']
# 在这里执行 CRUD 操作
# 关闭连接
client.close()
Create - 创建操作:插入数据
数据库的第一步就是插入数据。MongoDB 提供了两个核心方法:insertOne() 用于插入单个文档,insertMany() 用于批量插入。让我们从最简单的开始。
insertOne() - 插入单个文档
基础用法:
// MongoDB Shell / Node.js
// 插入一个用户文档
db.users.insertOne({
username: "zhangsan",
email: "zhangsan@example.com",
age: 28,
createdAt: new Date()
});
// 返回结果
{
acknowledged: true,
insertedId: ObjectId("507f1f77bcf86cd799439011")
}
完整示例:插入博客文章
// Node.js 示例
const article = {
title: "MongoDB CRUD 操作指南",
author: {
name: "张三",
email: "zhangsan@example.com",
avatar: "https://example.com/avatar.jpg",
},
content: "这是一篇关于 MongoDB CRUD 操作的详细教程...",
tags: ["MongoDB", "数据库", "教程"],
categories: ["技术", "后端开发"],
metadata: {
views: 0,
likes: 0,
comments: 0,
},
status: "published",
publishedAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
const result = db.articles.insertOne(article);
console.log(`新文章 ID: ${result.insertedId}`);
指定自定义 _id:
// MongoDB Shell / Node.js
// 使用自定义字符串作为 _id
db.products.insertOne({
_id: "PROD_001",
name: "iPhone 15 Pro",
price: NumberDecimal("7999.00"),
stock: 100,
});
// 使用自定义 ObjectId
db.orders.insertOne({
_id: ObjectId(),
orderId: "ORD_20240115_001",
userId: ObjectId("507f1f77bcf86cd799439011"),
totalAmount: NumberDecimal("1999.00"),
});
insertMany() - 插入多个文档
基础用法:
// 批量插入用户
const users = [
{
username: "lisi",
email: "lisi@example.com",
age: 25,
role: "user",
},
{
username: "wangwu",
email: "wangwu@example.com",
age: 30,
role: "admin",
},
{
username: "zhaoliu",
email: "zhaoliu@example.com",
age: 27,
role: "user",
},
];
const result = db.users.insertMany(users);
console.log(`插入了 ${result.insertedCount} 个文档`);
console.log("插入的 ID:", result.insertedIds);
有序插入 vs 无序插入:
// MongoDB Shell / Node.js
// 有序插入(默认):遇到错误会停止
db.products.insertMany(
[
{ _id: 1, name: "产品A" },
{ _id: 2, name: "产品B" },
{ _id: 1, name: "产品C" }, // 重复 ID,会报错
{ _id: 3, name: "产品D" }, // 不会被插入
],
{ ordered: true } // 默认为 true
);
// 无序插入:跳过错误,继续插入其他文档
db.products.insertMany(
[
{ _id: 1, name: "产品A" },
{ _id: 2, name: "产品B" },
{ _id: 1, name: "产品C" }, // 重复 ID,会报错
{ _id: 3, name: "产品D" }, // 会被插入
],
{ ordered: false }
);
实际应用:批量导入数据
// Node.js 示例
// 批量导入商品数据
const products = [];
for (let i = 1; i <= 1000; i++) {
products.push({
sku: `PROD_${String(i).padStart(6, "0")}`,
name: `商品 ${i}`,
price: NumberDecimal((Math.random() * 1000).toFixed(2)),
category: ["电子产品", "服装", "食品", "图书"][i % 4],
stock: Math.floor(Math.random() * 500),
createdAt: new Date(),
});
}
// 分批插入(MongoDB 单次最多插入 100000 个文档)
const batchSize = 1000;
for (let i = 0; i < products.length; i += batchSize) {
const batch = products.slice(i, i + batchSize);
db.products.insertMany(batch);
console.log(`插入批次 ${i / batchSize + 1}`);
}
插入操作的最佳实践
1. 总是验证数据
// Node.js 示例
// ❌ 不好的做法:直接插入未验证的数据
db.users.insertOne({
username: userInput,
email: emailInput,
});
// ✅ 好的做法:先验证再插入
function validateUser(user) {
if (!user.username || user.username.length < 3) {
throw new Error("用户名至少需要 3 个字符");
}
if (!user.email || !/\S+@\S+\.\S+/.test(user.email)) {
throw new Error("邮箱格式不正确");
}
return true;
}
try {
validateUser(newUser);
db.users.insertOne(newUser);
} catch (error) {
console.error("插入失败:", error.message);
}
2. 使用合适的数据类型
// MongoDB Shell / Node.js
// ❌ 不好的做法
db.orders.insertOne({
price: "99.99", // 字符串
quantity: "10", // 字符串
createdAt: "2024-01-15", // 字符串
});
// ✅ 好的做法
db.orders.insertOne({
price: NumberDecimal("99.99"), // 高精度小数
quantity: NumberInt(10), // 整数
createdAt: new Date("2024-01-15"), // 日期对象
});
3. 设置默认值
// Node.js 示例
// 插入时设置默认值
function createUser(userData) {
const defaultUser = {
role: "user",
status: "active",
credits: 0,
createdAt: new Date(),
updatedAt: new Date(),
settings: {
notifications: true,
theme: "light",
},
};
return db.users.insertOne({
...defaultUser,
...userData,
});
}
Read - 读取操作:查询数据
数据插入后,我们需要能够读取它们。MongoDB 的查询功能非常强大,从简单的精确匹配到复杂的条件组合都能轻松实现。
findOne() - 查询单个文档
基础用法:
// MongoDB Shell / Node.js
// 查询单个用户
db.users.findOne({ username: "zhangsan" });
// 按 _id 查询
db.users.findOne({ _id: ObjectId("507f1f77bcf86cd799439011") });
// 查询第一个匹配的文档
db.articles.findOne({ status: "published" });
投影:只返回需要的字段
// 只返回特定字段(1 表示包含,0 表示排除)
db.users.findOne(
{ username: "zhangsan" },
{ projection: { username: 1, email: 1, _id: 0 } }
);
// 结果:{ username: "zhangsan", email: "zhangsan@example.com" }
// 排除特定字段
db.users.findOne(
{ username: "zhangsan" },
{ projection: { password: 0, privateData: 0 } }
);
find() - 查询多个文档
基础用法:
// 查询所有文档
db.users.find();
// 条件查询
db.users.find({ age: { $gte: 18 } });
// 查询并转为数组
db.users.find({ role: "admin" }).toArray();
常用查询操作符:
// 1. 比较操作符
db.products.find({
price: { $gt: 100 }, // 大于 100
});
db.products.find({
price: { $gte: 100, $lte: 1000 }, // 100 到 1000 之间
});
db.products.find({
category: { $in: ["电子产品", "图书"] }, // 在指定列表中
});
db.products.find({
category: { $nin: ["食品"] }, // 不在指定列表中
});
db.products.find({
stock: { $ne: 0 }, // 不等于 0
});
// 2. 逻辑操作符
// $and:同时满足多个条件
db.products.find({
$and: [{ price: { $gte: 100 } }, { stock: { $gt: 0 } }, { status: "active" }],
});
// $or:满足任一条件
db.products.find({
$or: [{ category: "电子产品" }, { price: { $lt: 50 } }],
});
// $not:不满足条件
db.products.find({
price: { $not: { $gt: 1000 } },
});
// $nor:都不满足
db.products.find({
$nor: [{ stock: 0 }, { status: "discontinued" }],
});
// 3. 元素操作符
// $exists:字段是否存在
db.users.find({
phone: { $exists: true },
});
// $type:字段类型
db.products.find({
price: { $type: "decimal" },
});
// 4. 数组操作符
// $all:包含所有元素
db.articles.find({
tags: { $all: ["MongoDB", "教程"] },
});
// $elemMatch:数组元素匹配
db.orders.find({
items: {
$elemMatch: {
product: "iPhone",
quantity: { $gte: 2 },
},
},
});
// $size:数组长度
db.articles.find({
tags: { $size: 3 },
});
// 5. 字符串操作符
// $regex:正则表达式匹配
db.users.find({
email: { $regex: /@gmail\.com$/, $options: "i" },
});
// $text:全文搜索(需要创建文本索引)
db.articles.find({
$text: { $search: "MongoDB 教程" },
});
嵌套文档查询:
// 精确匹配整个嵌套文档
db.users.find({
address: {
city: "北京",
district: "朝阳区",
},
});
// 使用点号查询嵌套字段(推荐)
db.users.find({
"address.city": "北京",
"address.district": "朝阳区",
});
// 查询数组中的嵌套文档
db.orders.find({
"items.product": "iPhone",
"items.price": { $gt: 5000 },
});
排序、限制和跳过:
// 排序:1 升序,-1 降序
db.products.find().sort({ price: -1, createdAt: -1 });
// 限制返回数量
db.products.find().sort({ price: -1 }).limit(10);
// 跳过指定数量(分页)
db.products.find().sort({ createdAt: -1 }).skip(20).limit(10);
// 链式调用
db.articles
.find({ status: "published" })
.sort({ publishedAt: -1 })
.limit(5)
.projection({ title: 1, author: 1, publishedAt: 1 });
分页查询实现:
// 分页函数
function getPaginatedResults(collection, query, page, pageSize) {
const skip = (page - 1) * pageSize;
return {
data: collection
.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(pageSize)
.toArray(),
total: collection.countDocuments(query),
page: page,
pageSize: pageSize,
totalPages: Math.ceil(collection.countDocuments(query) / pageSize),
};
}
// 使用示例
const result = getPaginatedResults(
db.articles,
{ status: "published" },
1, // 第1页
20 // 每页20条
);
查询性能优化
1. 使用投影减少数据传输
// ❌ 查询所有字段(数据量大)
db.users.find({ age: { $gte: 18 } });
// ✅ 只查询需要的字段
db.users.find({ age: { $gte: 18 } }, { projection: { username: 1, email: 1 } });
2. 创建合适的索引
// 为常用查询字段创建索引
db.users.createIndex({ email: 1 });
db.products.createIndex({ category: 1, price: -1 });
db.articles.createIndex({ status: 1, publishedAt: -1 });
// 查看查询使用的索引
db.users.find({ email: "test@example.com" }).explain("executionStats");
3. 使用 limit 限制结果数量
// ❌ 查询所有匹配的文档
const users = db.users.find({ status: "active" });
// ✅ 限制返回数量
const users = db.users.find({ status: "active" }).limit(100);
Update - 更新操作:修改数据
数据不是静态的,我们经常需要修改已有数据。MongoDB 提供了丰富的更新操作符,让数据更新变得灵活而强大。
updateOne() - 更新单个文档
基础用法:
// 更新用户年龄
db.users.updateOne(
{ username: "zhangsan" }, // 查询条件
{ $set: { age: 29 } } // 更新操作
);
// 返回结果
{
acknowledged: true,
matchedCount: 1, // 匹配到的文档数
modifiedCount: 1, // 实际修改的文档数
upsertedId: null
}
常用更新操作符:
// 1. $set:设置字段值
db.users.updateOne(
{ username: "zhangsan" },
{
$set: {
email: "new_email@example.com",
"address.city": "上海",
updatedAt: new Date(),
},
}
);
// 2. $unset:删除字段
db.users.updateOne(
{ username: "zhangsan" },
{
$unset: {
tempField: "", // 值可以是任意内容
oldData: "",
},
}
);
// 3. $inc:增加数值
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$inc: {
"metadata.views": 1, // 浏览量 +1
"metadata.likes": 1, // 点赞数 +1
},
}
);
// 4. $mul:乘以数值
db.products.updateOne(
{ _id: "PROD_001" },
{
$mul: {
price: 0.8, // 价格打 8 折
},
}
);
// 5. $min:如果新值小于当前值则更新
db.products.updateOne(
{ _id: "PROD_001" },
{
$min: {
lowestPrice: NumberDecimal("799.00"),
},
}
);
// 6. $max:如果新值大于当前值则更新
db.products.updateOne(
{ _id: "PROD_001" },
{
$max: {
highestPrice: NumberDecimal("9999.00"),
},
}
);
// 7. $rename:重命名字段
db.users.updateOne(
{ username: "zhangsan" },
{
$rename: {
addr: "address",
tel: "phone",
},
}
);
// 8. $currentDate:设置当前日期
db.users.updateOne(
{ username: "zhangsan" },
{
$currentDate: {
lastLogin: true, // 使用 Date 类型
lastModified: { $type: "timestamp" }, // 使用 Timestamp 类型
},
}
);
数组更新操作符:
// 1. $push:向数组末尾添加元素
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$push: {
tags: "新标签",
},
}
);
// 2. $push + $each:添加多个元素
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$push: {
tags: {
$each: ["标签1", "标签2", "标签3"],
},
},
}
);
// 3. $push + $position:在指定位置插入
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$push: {
tags: {
$each: ["重要"],
$position: 0, // 插入到数组开头
},
},
}
);
// 4. $addToSet:添加元素(避免重复)
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$addToSet: {
tags: "MongoDB", // 如果已存在则不添加
},
}
);
// 5. $addToSet + $each:添加多个唯一元素
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$addToSet: {
tags: {
$each: ["MongoDB", "数据库", "NoSQL"],
},
},
}
);
// 6. $pop:删除数组首尾元素
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$pop: {
tags: 1, // 删除最后一个元素(-1 删除第一个)
},
}
);
// 7. $pull:删除匹配的元素
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$pull: {
tags: "旧标签",
},
}
);
// 8. $pullAll:删除多个指定元素
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$pullAll: {
tags: ["标签1", "标签2"],
},
}
);
// 9. $ 位置操作符:更新匹配的数组元素
db.orders.updateOne(
{
_id: ObjectId("..."),
"items.product": "iPhone",
},
{
$set: {
"items.$.quantity": 5, // 更新匹配的第一个元素
},
}
);
// 10. $[] 所有位置操作符:更新所有数组元素
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$inc: {
"comments.$[].likes": 1, // 所有评论的点赞数 +1
},
}
);
// 11. $[<identifier>] 过滤位置操作符:更新满足条件的元素
db.articles.updateOne(
{ _id: ObjectId("...") },
{
$set: {
"comments.$[elem].status": "approved",
},
},
{
arrayFilters: [{ "elem.likes": { $gte: 10 } }],
}
);
updateMany() - 更新多个文档
基础用法:
// 更新所有匹配的文档
db.products.updateMany(
{ category: "电子产品" },
{
$set: {
taxRate: 0.13,
updatedAt: new Date()
}
}
);
// 返回结果
{
acknowledged: true,
matchedCount: 150,
modifiedCount: 150,
upsertedId: null
}
实际应用示例:
// 1. 批量更新价格
db.products.updateMany(
{ category: "服装", season: "冬季" },
{
$mul: { price: 0.5 }, // 所有冬季服装打 5 折
$set: { onSale: true, updatedAt: new Date() },
}
);
// 2. 批量修改状态
db.orders.updateMany(
{
status: "pending",
createdAt: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
},
{
$set: {
status: "expired",
expiredAt: new Date(),
},
}
);
// 3. 批量添加字段
db.users.updateMany(
{ vipLevel: { $exists: false } },
{
$set: {
vipLevel: 0,
vipExpireDate: null,
},
}
);
replaceOne() - 替换整个文档
// 完整替换文档(保留 _id)
db.products.replaceOne(
{ _id: "PROD_001" },
{
name: "新产品名称",
price: NumberDecimal("999.00"),
category: "新分类",
description: "全新的产品描述",
updatedAt: new Date(),
}
);
// 注意:replaceOne 会删除原文档的所有其他字段
// 只保留 _id 和新提供的字段
findOneAndUpdate() - 原子性查找并更新
// 查找并更新,返回更新后的文档
const result = db.counters.findOneAndUpdate(
{ _id: "orderCounter" },
{ $inc: { sequence: 1 } },
{
returnDocument: "after", // 返回更新后的文档
upsert: true, // 如果不存在则创建
}
);
console.log(`新的订单号: ORD_${result.sequence}`);
// 实际应用:生成唯一订单号
function generateOrderId() {
const counter = db.counters.findOneAndUpdate(
{ _id: "orderCounter" },
{ $inc: { value: 1 } },
{
returnDocument: "after",
upsert: true,
}
);
return `ORD_${new Date().getFullYear()}${String(counter.value).padStart(
8,
"0"
)}`;
}
upsert - 不存在则插入
// upsert: true - 如果文档不存在则插入
db.users.updateOne(
{ username: "newuser" },
{
$set: {
email: "newuser@example.com",
createdAt: new Date(),
},
$setOnInsert: {
role: "user",
status: "active",
},
},
{ upsert: true }
);
// $setOnInsert:仅在插入时设置的字段
更新操作的最佳实践
1. 始终更新 updatedAt 字段
// ✅ 好的做法:记录更新时间
db.users.updateOne(
{ _id: userId },
{
$set: {
email: newEmail,
updatedAt: new Date(),
},
}
);
2. 使用原子操作避免竞态条件
// ❌ 不好的做法:分两步操作(可能导致数据不一致)
const product = db.products.findOne({ _id: productId });
product.stock -= quantity;
db.products.updateOne({ _id: productId }, { $set: { stock: product.stock } });
// ✅ 好的做法:使用原子操作
db.products.updateOne(
{ _id: productId, stock: { $gte: quantity } },
{
$inc: { stock: -quantity },
$push: {
transactions: {
type: "sale",
quantity: quantity,
timestamp: new Date(),
},
},
}
);
3. 验证更新结果
// 检查更新是否成功
const result = db.users.updateOne(
{ _id: userId },
{ $set: { email: newEmail } }
);
if (result.matchedCount === 0) {
console.error("用户不存在");
} else if (result.modifiedCount === 0) {
console.log("数据未发生变化");
} else {
console.log("更新成功");
}
Delete - 删除操作:移除数据
删除操作要格外谨慎,因为数据一旦删除就很难恢复。让我们学习如何安全、正确地删除数据。
deleteOne() - 删除单个文档
基础用法:
// 删除单个用户
db.users.deleteOne({ username: "zhangsan" });
// 返回结果
{
acknowledged: true,
deletedCount: 1
}
// 按 _id 删除
db.articles.deleteOne({ _id: ObjectId("507f1f77bcf86cd799439011") });
实际应用示例:
// 1. 删除过期数据
db.sessions.deleteOne({
_id: sessionId,
expiresAt: { $lt: new Date() },
});
// 2. 软删除(标记为删除而不是真正删除)
db.users.updateOne(
{ _id: userId },
{
$set: {
deleted: true,
deletedAt: new Date(),
},
}
);
deleteMany() - 删除多个文档
基础用法:
// 删除所有匹配的文档
db.logs.deleteMany({
createdAt: { $lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
});
// 返回结果
{
acknowledged: true,
deletedCount: 1523
}
实际应用示例:
// 1. 清理过期会话
db.sessions.deleteMany({
expiresAt: { $lt: new Date() },
});
// 2. 批量删除测试数据
db.users.deleteMany({
email: { $regex: /@test\.com$/ },
});
// 3. 删除所有文档(慎用!)
db.tempCollection.deleteMany({});
// 4. 带条件的批量删除
db.orders.deleteMany({
status: "cancelled",
createdAt: { $lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) },
});
findOneAndDelete() - 原子性查找并删除
// 查找并删除,返回被删除的文档
const deletedUser = db.users.findOneAndDelete({
username: "zhangsan",
});
console.log("已删除的用户:", deletedUser);
// 实际应用:实现消息队列
function dequeueMessage() {
return db.messageQueue.findOneAndDelete(
{ status: "pending" },
{ sort: { priority: -1, createdAt: 1 } }
);
}
删除操作的最佳实践
1. 使用软删除代替硬删除
// ❌ 硬删除:数据无法恢复
db.users.deleteOne({ _id: userId });
// ✅ 软删除:保留数据,便于恢复和审计
db.users.updateOne(
{ _id: userId },
{
$set: {
deleted: true,
deletedAt: new Date(),
deletedBy: currentUserId,
},
}
);
// 查询时排除已删除的数据
db.users.find({ deleted: { $ne: true } });
2. 删除前进行确认
// 先查询再删除
const user = db.users.findOne({ _id: userId });
if (user) {
console.log("即将删除:", user.username);
// 确认后再删除
db.users.deleteOne({ _id: userId });
}
3. 级联删除相关数据
// 删除用户及其相关数据
function deleteUserCascade(userId) {
// 1. 删除用户的文章
db.articles.deleteMany({ authorId: userId });
// 2. 删除用户的评论
db.comments.deleteMany({ userId: userId });
// 3. 从其他用户的关注列表中移除
db.users.updateMany({ following: userId }, { $pull: { following: userId } });
// 4. 最后删除用户
db.users.deleteOne({ _id: userId });
return { success: true, message: "用户及相关数据已删除" };
}
4. 记录删除日志
// 删除前记录日志
function deleteWithLog(collection, query, operator) {
const document = collection.findOne(query);
if (document) {
// 记录删除日志
db.deletionLogs.insertOne({
collection: collection.getName(),
documentId: document._id,
document: document,
deletedBy: operator,
deletedAt: new Date(),
});
// 执行删除
return collection.deleteOne(query);
}
}
批量操作:bulkWrite()
当你需要执行多种不同类型的操作(插入、更新、删除混合)时,bulkWrite() 是最高效的选择。它可以在一次请求中完成多个操作。
bulkWrite() - 高效的批量操作
// 批量执行多种操作
db.products.bulkWrite([
// 插入
{
insertOne: {
document: {
name: "新产品",
price: NumberDecimal("199.00"),
},
},
},
// 更新
{
updateOne: {
filter: { _id: "PROD_001" },
update: { $set: { stock: 100 } },
},
},
// 删除
{
deleteOne: {
filter: { _id: "PROD_999" },
},
},
// 替换
{
replaceOne: {
filter: { _id: "PROD_002" },
replacement: {
name: "替换后的产品",
price: NumberDecimal("299.00"),
},
},
},
]);
有序 vs 无序批量操作:
// 有序操作(默认):按顺序执行,遇错即停
db.products.bulkWrite([...operations], { ordered: true });
// 无序操作:并行执行,跳过错误
db.products.bulkWrite([...operations], { ordered: false });
实际应用:批量导入和更新
// 批量导入/更新产品数据
function bulkImportProducts(products) {
const operations = products.map((product) => ({
updateOne: {
filter: { sku: product.sku },
update: {
$set: {
name: product.name,
price: NumberDecimal(product.price.toString()),
stock: product.stock,
updatedAt: new Date(),
},
$setOnInsert: {
createdAt: new Date(),
},
},
upsert: true,
},
}));
return db.products.bulkWrite(operations);
}
实际应用场景
理论掌握后,让我们看看如何在真实业务场景中应用这些 CRUD 操作。以下是三个典型的应用案例。
场景 1:用户注册和登录系统
用户注册:
// Node.js 示例 - 完整的用户注册流程
async function registerUser(userData) {
// 1. 检查用户名是否已存在
const existingUser = await db.users.findOne({
username: userData.username,
});
if (existingUser) {
throw new Error("用户名已存在");
}
// 2. 检查邮箱是否已存在
const existingEmail = await db.users.findOne({
email: userData.email,
});
if (existingEmail) {
throw new Error("邮箱已被注册");
}
// 3. 创建新用户
const newUser = {
username: userData.username,
email: userData.email,
password: await hashPassword(userData.password),
profile: {
displayName: userData.username,
avatar: "/default-avatar.png",
},
role: "user",
status: "active",
credits: 0,
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await db.users.insertOne(newUser);
return {
userId: result.insertedId,
username: newUser.username,
};
}
用户登录:
// Node.js 示例 - 用户登录验证流程
async function loginUser(username, password) {
// 1. 查找用户
const user = await db.users.findOne({
$or: [{ username: username }, { email: username }],
status: "active",
});
if (!user) {
throw new Error("用户不存在或已被禁用");
}
// 2. 验证密码
const isPasswordValid = await verifyPassword(password, user.password);
if (!isPasswordValid) {
// 记录登录失败
await db.users.updateOne(
{ _id: user._id },
{
$inc: { "loginAttempts.failed": 1 },
$set: { "loginAttempts.lastFailedAt": new Date() },
}
);
throw new Error("密码错误");
}
// 3. 更新登录信息
await db.users.updateOne(
{ _id: user._id },
{
$set: {
lastLoginAt: new Date(),
"loginAttempts.failed": 0,
},
}
);
return {
userId: user._id,
username: user.username,
role: user.role,
};
}
场景 2:电商订单系统
创建订单:
// Node.js 示例 - 带事务的订单创建流程
async function createOrder(userId, items) {
const session = db.getMongo().startSession();
try {
session.startTransaction();
// 1. 检查库存并扣减
for (const item of items) {
const product = await db.products.findOne(
{ _id: item.productId },
{ session }
);
if (!product || product.stock < item.quantity) {
throw new Error(`商品 ${item.productId} 库存不足`);
}
// 扣减库存
await db.products.updateOne(
{
_id: item.productId,
stock: { $gte: item.quantity },
},
{
$inc: { stock: -item.quantity },
$push: {
stockHistory: {
type: "sale",
quantity: -item.quantity,
timestamp: new Date(),
},
},
},
{ session }
);
}
// 2. 创建订单
const order = {
userId: userId,
items: items.map((item) => ({
productId: item.productId,
name: item.name,
price: item.price,
quantity: item.quantity,
subtotal: item.price * item.quantity,
})),
totalAmount: items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
status: "pending",
paymentStatus: "unpaid",
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await db.orders.insertOne(order, { session });
await session.commitTransaction();
return {
orderId: result.insertedId,
order: order,
};
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
更新订单状态:
async function updateOrderStatus(orderId, newStatus) {
const result = await db.orders.findOneAndUpdate(
{ _id: orderId },
{
$set: {
status: newStatus,
updatedAt: new Date(),
},
$push: {
statusHistory: {
status: newStatus,
timestamp: new Date(),
},
},
},
{ returnDocument: "after" }
);
if (!result) {
throw new Error("订单不存在");
}
return result;
}
场景 3:博客评论系统
添加评论:
// Node.js 示例 - 博客评论功能
async function addComment(articleId, userId, content) {
const comment = {
commentId: new ObjectId(),
userId: userId,
content: content,
likes: 0,
createdAt: new Date(),
};
const result = await db.articles.updateOne(
{ _id: articleId },
{
$push: { comments: comment },
$inc: { "metadata.comments": 1 },
}
);
if (result.matchedCount === 0) {
throw new Error("文章不存在");
}
return comment;
}
点赞评论:
async function likeComment(articleId, commentId, userId) {
const result = await db.articles.updateOne(
{
_id: articleId,
"comments.commentId": commentId,
"comments.likedBy": { $ne: userId }, // 防止重复点赞
},
{
$inc: { "comments.$.likes": 1 },
$push: { "comments.$.likedBy": userId },
}
);
return result.modifiedCount > 0;
}
企业开发中的常见问题与解决方案
掌握了基础 CRUD 操作和实际应用案例后,让我们面对现实:在企业级生产环境中,MongoDB 会遇到哪些挑战?如何解决?
企业级 MongoDB 面临的主要挑战
在企业开发中,MongoDB 虽然提供了灵活性和高性能,但也会遇到一些实际挑战:
1. 数据一致性问题
- 挑战:MongoDB 默认是最终一致性,不像 MySQL 的强一致性
- 影响:金融、订单等关键业务场景可能出现数据不一致
2. 事务支持的局限性
- 挑战:虽然 MongoDB 4.0+ 支持多文档事务,但性能不如关系型数据库
- 影响:复杂业务逻辑需要额外的设计考虑
3. 内存占用问题
- 挑战:MongoDB 会将热数据加载到内存,内存占用较大
- 影响:服务器成本增加,需要合理规划内存
4. 索引管理复杂
- 挑战:索引过多影响写性能,索引过少影响查询性能
- 影响:需要持续优化和监控
5. 数据迁移困难
- 挑战:文档结构灵活,但大规模数据迁移复杂
- 影响:版本升级和数据重构成本高
6. 连接数限制
- 挑战:默认连接数有限,高并发场景可能不足
- 影响:需要优化连接池配置
7. 备份恢复耗时
- 挑战:大数据量备份和恢复时间长
- 影响:影响灾难恢复的 RTO/RPO
问题 1:如何避免重复插入?
解决方案:使用唯一索引
// MongoDB Shell / Node.js
// 1. 创建唯一索引
db.users.createIndex({ email: 1 }, { unique: true });
// 2. 插入时会自动检查唯一性
try {
db.users.insertOne({
email: "test@example.com",
username: "testuser",
});
} catch (error) {
if (error.code === 11000) {
console.log("邮箱已存在");
}
}
// 3. 或使用 upsert
db.users.updateOne(
{ email: "test@example.com" },
{ $set: { username: "testuser" } },
{ upsert: true }
);
问题 2:如何实现分页查询?
解决方案 1:skip + limit(适用于小数据量)
// Node.js 示例 - 传统分页方式
function paginate(page, pageSize) {
const skip = (page - 1) * pageSize;
return db.articles
.find({ status: "published" })
.sort({ publishedAt: -1 })
.skip(skip)
.limit(pageSize);
}
解决方案 2:基于游标的分页(适用于大数据量)
// Node.js 示例 - 游标分页方式(推荐)
function paginateWithCursor(lastId, pageSize) {
const query = lastId ? { _id: { $gt: lastId } } : {};
return db.articles.find(query).sort({ _id: 1 }).limit(pageSize);
}
问题 3:如何处理大量数据的更新?
解决方案:批量操作
// Node.js 示例 - 大数据量分批更新
// 分批更新
async function bulkUpdate(filter, update, batchSize = 1000) {
let processedCount = 0;
let hasMore = true;
while (hasMore) {
const result = await db.collection.updateMany(
{ ...filter, processed: { $ne: true } },
{
...update,
$set: { processed: true },
}
);
processedCount += result.modifiedCount;
hasMore = result.matchedCount === batchSize;
console.log(`已处理 ${processedCount} 条记录`);
}
return processedCount;
}
问题 4:如何保证数据一致性?
解决方案:使用事务
// Node.js 示例 - 使用事务保证数据一致性
async function transferCredits(fromUserId, toUserId, amount) {
const session = db.getMongo().startSession();
try {
session.startTransaction();
// 1. 扣减发送方积分
const result1 = await db.users.updateOne(
{
_id: fromUserId,
credits: { $gte: amount },
},
{
$inc: { credits: -amount },
},
{ session }
);
if (result1.modifiedCount === 0) {
throw new Error("积分不足");
}
// 2. 增加接收方积分
await db.users.updateOne(
{ _id: toUserId },
{
$inc: { credits: amount },
},
{ session }
);
// 3. 记录转账记录
await db.transactions.insertOne(
{
from: fromUserId,
to: toUserId,
amount: amount,
type: "transfer",
createdAt: new Date(),
},
{ session }
);
await session.commitTransaction();
return { success: true };
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
企业级 CRUD 操作解决方案
了解了挑战,现在让我们逐一击破。以下是针对上述 7 大挑战的实战解决方案。
解决方案 1:数据一致性保障
使用事务处理关键业务
// Node.js 示例 - 企业级订单支付流程
// 包含完整的事务处理和错误回滚机制
async function processPayment(orderId, paymentInfo) {
const session = db.getMongo().startSession();
try {
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
readPreference: "primary",
});
// 1. 验证订单状态
const order = await db.orders.findOne(
{ _id: orderId, paymentStatus: "unpaid" },
{ session }
);
if (!order) {
throw new Error("订单不存在或已支付");
}
// 2. 扣减用户余额
const result = await db.users.updateOne(
{
_id: order.userId,
balance: { $gte: order.totalAmount },
},
{
$inc: { balance: -order.totalAmount },
},
{ session }
);
if (result.modifiedCount === 0) {
throw new Error("余额不足");
}
// 3. 更新订单状态
await db.orders.updateOne(
{ _id: orderId },
{
$set: {
paymentStatus: "paid",
paymentInfo: paymentInfo,
paidAt: new Date(),
},
},
{ session }
);
// 4. 记录支付日志
await db.paymentLogs.insertOne(
{
orderId: orderId,
userId: order.userId,
amount: order.totalAmount,
status: "success",
createdAt: new Date(),
},
{ session }
);
await session.commitTransaction();
return { success: true, orderId: orderId };
} catch (error) {
await session.abortTransaction();
console.error("支付失败:", error);
throw error;
} finally {
session.endSession();
}
}
解决方案 2:高并发场景优化
连接池配置优化
// Node.js 驱动连接池配置
const client = new MongoClient(uri, {
maxPoolSize: 100, // 最大连接数
minPoolSize: 10, // 最小连接数
maxIdleTimeMS: 30000, // 空闲连接超时
waitQueueTimeoutMS: 5000, // 等待连接超时
});
// 监控连接池状态
setInterval(() => {
const poolStats = client.db().admin().serverStatus();
console.log("当前连接数:", poolStats.connections.current);
console.log("可用连接数:", poolStats.connections.available);
}, 60000);
批量写入优化
// 高性能批量插入
async function bulkInsertOptimized(documents, batchSize = 1000) {
const results = [];
for (let i = 0; i < documents.length; i += batchSize) {
const batch = documents.slice(i, i + batchSize);
try {
const result = await db.collection.insertMany(batch, {
ordered: false, // 无序插入,提升性能
writeConcern: { w: 1 }, // 降低写确认级别
});
results.push(result);
} catch (error) {
console.error(`批次 ${i / batchSize + 1} 插入失败:`, error);
}
}
return results;
}
解决方案 3:内存管理优化
控制查询内存占用
// ❌ 不好的做法:一次性加载所有数据
const allUsers = await db.users.find({}).toArray();
// ✅ 好的做法:使用游标分批处理
const cursor = db.users.find({}).batchSize(100);
while (await cursor.hasNext()) {
const user = await cursor.next();
// 处理单个用户
await processUser(user);
}
// ✅ 更好的做法:使用聚合管道的游标
const cursor = db.users.aggregate(
[{ $match: { status: "active" } }, { $project: { username: 1, email: 1 } }],
{
allowDiskUse: true, // 允许使用磁盘
cursor: { batchSize: 100 },
}
);
解决方案 4:索引策略管理
建立索引管理规范
// 创建复合索引
db.orders.createIndex(
{ userId: 1, status: 1, createdAt: -1 },
{
name: "idx_user_status_date",
background: true, // 后台创建,不阻塞操作
}
);
// 定期检查索引使用情况
db.orders.aggregate([{ $indexStats: {} }]);
// 删除未使用的索引
db.orders.dropIndex("unused_index_name");
// 索引监控脚本
function monitorIndexes(collectionName) {
const stats = db[collectionName].stats();
return {
indexCount: stats.nindexes,
indexSize: stats.indexSizes,
totalIndexSizeMB: stats.totalIndexSize / 1024 / 1024,
dataToIndexRatio: stats.totalIndexSize / stats.size,
};
}
解决方案 5:数据迁移策略
安全的数据迁移流程
// Node.js 示例 - 数据迁移脚本
// 包含数据验证、清洗和错误处理
async function migrateData(sourceDB, targetDB) {
const cursor = sourceDB.collection("oldUsers").find({});
let migratedCount = 0;
let errorCount = 0;
while (await cursor.hasNext()) {
const oldDoc = await cursor.next();
try {
// 数据清洗和转换
const newDoc = {
_id: oldDoc._id,
username: oldDoc.user_name, // 字段重命名
email: oldDoc.email,
age: NumberInt(oldDoc.age), // 类型转换
balance: NumberDecimal(oldDoc.balance.toString()),
createdAt: new Date(oldDoc.create_time),
migratedAt: new Date(),
};
// 插入到新集合
await targetDB.collection("users").insertOne(newDoc);
migratedCount++;
} catch (error) {
errorCount++;
// 记录失败的文档
await targetDB.collection("migrationErrors").insertOne({
sourceDoc: oldDoc,
error: error.message,
timestamp: new Date(),
});
}
}
return { migratedCount, errorCount };
}
下一篇文章,我们将深入探讨 MongoDB 的查询语法,学习如何编写高效的复杂查询,掌握数据检索的高级技巧。敬请期待!
876

被折叠的 条评论
为什么被折叠?



