这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。
CRUD 是 MongoDB 最常用的操作。核心原则是:写操作明确 _id 和更新策略,读操作明确投影、排序和分页,批量操作控制并发,所有高频查询都有索引支撑。
4.1 插入文档
插入单条:
db.orders.insertOne({
_id: "o_202608250001",
user_id: "u_1001",
status: "CREATED",
amount: NumberDecimal("199.00"),
items: [
{ sku_id: "sku_10001", quantity: 1, price: NumberDecimal("199.00") }
],
created_at: new Date()
})
插入多条:
db.products.insertMany([
{ sku_id: "sku_10001", title: "键盘", price: NumberDecimal("399.00") },
{ sku_id: "sku_10002", title: "鼠标", price: NumberDecimal("199.00") }
], { ordered: false })
ordered: false 允许继续处理后续文档,但应用仍需处理重复键等错误。
4.2 查询文档
查询单条:
db.orders.findOne({ _id: "o_202608250001" })
查询多条:
db.orders.find({
user_id: "u_1001",
status: { $in: ["CREATED", "PAID"] }
})
指定字段:
db.orders.find(
{ user_id: "u_1001" },
{ order_no: 1, status: 1, amount: 1, created_at: 1, _id: 0 }
)
排序和限制:
db.orders.find({ user_id: "u_1001" })
.sort({ created_at: -1 })
.skip(0)
.limit(20)
大分页不建议使用大 skip,应使用范围条件游标分页。
4.3 更新文档
更新单条:
db.orders.updateOne(
{ _id: "o_202608250001", status: "CREATED" },
{
$set: { status: "PAID", paid_at: new Date() },
$currentDate: { updated_at: true }
}
)
更新多条:
db.products.updateMany(
{ status: "ON_SALE" },
{ $set: { channel: "online" } }
)
常用更新操作符:
| 操作符 | 说明 |
|---|---|
$set |
设置字段 |
$unset |
删除字段 |
$inc |
数值递增 |
$mul |
数值相乘 |
$push |
数组添加 |
$pull |
数组删除 |
$addToSet |
去重添加 |
$currentDate |
更新时间 |
不要用不带更新操作符的文档整体替换,除非确实要覆盖。
4.4 数组更新
添加标签:
db.products.updateOne(
{ sku_id: "sku_10001" },
{ $addToSet: { tags: "hot" } }
)
移除标签:
db.products.updateOne(
{ sku_id: "sku_10001" },
{ $pull: { tags: "cold" } }
)
更新数组中匹配元素:
db.orders.updateOne(
{
_id: "o_202608250001",
"items.sku_id": "sku_10001"
},
{
$set: { "items.$.quantity": 2 }
}
)
数组位置更新要确认匹配条件唯一,否则可能更新到非预期元素。
4.5 删除文档
删除单条:
db.orders.deleteOne({ _id: "o_202608250001" })
删除多条:
db.orders.deleteMany({
status: "CLOSED",
created_at: { $lt: new Date("2026-01-01T00:00:00Z") }
})
删除建议:
- 生产删除前先查询确认数量;
- 保留审计数据时使用软删除;
- 大量删除分批执行;
- 删除条件必须有索引;
- 删除不可轻易回滚。
4.6 批量写入
db.orders.bulkWrite([
{ insertOne: { document: { _id: "o_1", status: "CREATED" } } },
{ updateOne: {
filter: { _id: "o_1", status: "CREATED" },
update: { $set: { status: "PAID" } }
}},
{ deleteOne: { filter: { _id: "o_2", status: "CANCELLED" } } }
], { ordered: false })
批量写入可以减少网络往返,但要控制批次大小,并处理每类错误。
4.7 查找并修改
const result = db.orders.findOneAndUpdate(
{ _id: "o_202608250001", status: "CREATED" },
{ $set: { status: "PROCESSING" } },
{
returnDocument: "after",
projection: { status: 1 },
sort: { created_at: 1 }
}
)
适用:
- 原子领取任务;
- 状态机流转;
- 计数器更新;
- 避免读改写竞争。
类似命令还有 findOneAndReplace、findOneAndDelete。
4.8 乐观并发控制
文档版本字段:
db.orders.updateOne(
{ _id: "o_202608250001", version: 3 },
{
$set: { status: "PAID" },
$inc: { version: 1 }
}
)
若 matchedCount 为 0,说明版本变化或状态不满足条件,应重新读取业务对象。
4.9 写关注
插入:
db.orders.insertOne(
{ _id: "o_202608250003", status: "CREATED" },
{ writeConcern: { w: "majority", j: true, wtimeout: 3000 } }
)
| 参数 | 说明 |
|---|---|
w |
等待多少节点确认 |
j |
是否等待 journal |
wtimeout |
等待超时 |
重要交易数据通常使用 majority 写关注;日志类数据可以根据成本选择较低确认级别。
4.10 常见错误
| 问题 | 原因 |
|---|---|
| Duplicate key | _id 或唯一索引重复 |
| Document validation failure | validator 拒绝 |
| Query exceeded memory | 聚合或排序无索引 |
| Update matched 0 | 条件不匹配或版本变化 |
| Modified 0 | 值已相同 |
| WriteConcernTimeout | 副本确认超时 |
| Not primary | 写入 non-primary |
本章小结
MongoDB CRUD 的关键是把“条件更新”作为默认思维方式,用状态机条件、版本号和原子更新避免读改写竞争。读操作应始终控制投影、排序和数量;写操作要明确写关注、批量边界和错误处理。
思考题
updateOne和整体替换有什么区别?- 为什么大分页不推荐大
skip? findOneAndUpdate适合什么场景?- 版本号如何实现乐观锁?
- majority 写关注解决什么问题?