掌握MongoDB CRUD操作:实用命令指南

通过这份实用的命令指南,解锁MongoDB的核心CRUD操作。学习如何使用`insert`、`find`、`update`和`delete`命令高效管理数据。本文提供清晰的解释、真实案例以及创建、读取、更新和删除MongoDB集合中文档的最佳实践。无论是开发者还是管理员,这都是掌握MongoDB数据操作的必备资源。

掌握MongoDB CRUD操作:实用命令指南

MongoDB作为一种流行的NoSQL文档数据库,因其灵活性、可扩展性和高性能,成为无数现代应用的基石。与任何数据库交互的核心都是基本的创建、读取、更新和删除(CRUD)操作。掌握这些命令对于任何使用MongoDB的人来说都至关重要,无论是构建新功能的开发者,还是管理数据的管理员。

本指南使用一个小型的users集合,展示你在实际工作中会用到的MongoDB CRUD命令:从注册表单添加记录、查找支持工单背后的文档、在不覆盖文档其余部分的情况下更改字段,以及在不意外清空集合的情况下删除数据。示例使用mongosh,但相同的过滤器和更新操作符也适用于应用程序驱动程序。

前提条件

在深入命令之前,请确保你已具备:

  • MongoDB已安装并运行:你可以从MongoDB官方网站下载,或使用MongoDB Atlas等云服务。
  • 已安装mongosh(MongoDB Shell):这是MongoDB的交互式JavaScript接口。

连接到MongoDB并选择数据库

首先,打开终端或命令提示符,使用mongosh连接到你的MongoDB实例:

mongosh

连接后,你将进入默认的test数据库。要切换或创建新数据库,请使用use命令:

use myDatabase;

如果myDatabase不存在,当你向其中的集合插入第一个文档时,MongoDB会隐式创建它。

创建操作(插入)

创建操作涉及向集合中添加新文档。MongoDB提供了插入单个或多个文档的方法。

1. db.collection.insertOne()

此方法向集合中插入单个文档。如果集合不存在,MongoDB会创建它。

// 选择名为'users'的集合
db.users.insertOne({
  name: "Alice Smith",
  age: 30,
  city: "New York",
  email: "[email protected]",
  interests: ["reading", "hiking"]
});

输出将显示acknowledged状态和新文档的insertedId

2. db.collection.insertMany()

使用此方法在单个操作中向集合插入多个文档。它接受一个文档数组。

db.users.insertMany([
  {
    name: "Bob Johnson",
    age: 24,
    city: "Los Angeles",
    email: "[email protected]",
    interests: ["coding", "gaming"]
  },
  {
    name: "Charlie Brown",
    age: 35,
    city: "New York",
    email: "[email protected]",
    interests: ["cooking", "photography"]
  },
  {
    name: "Diana Prince",
    age: 29,
    city: "London",
    email: "[email protected]",
    interests: ["fitness", "travel"]
  }
]);

这将返回所有添加文档的insertedIds数组。

提示:如果你不提供_id字段,MongoDB会自动为每个文档添加一个唯一的ObjectId。

读取操作(查找)

读取操作涉及从集合中查询文档。find()方法是你的主要工具。

1. db.collection.find()

a. 查找所有文档

要检索集合中的所有文档,调用不带任何参数的find()

db.users.find();

b. 使用查询过滤器查找文档

find()传递一个查询文档以指定选择文档的条件。这相当于SQL中的WHERE子句。

// 查找来自纽约的用户
db.users.find({ city: "New York" });

// 查找年龄大于25的用户
db.users.find({ age: { $gt: 25 } });

// 查找年龄在25到35之间(不包括35)的用户
db.users.find({ age: { $gt: 25, $lt: 35 } });

// 查找兴趣包含'coding'的用户
db.users.find({ interests: "coding" });

// 查找兴趣同时包含'reading'和'hiking'的用户
db.users.find({ interests: { $all: ["reading", "hiking"] } });

// 查找名为Alice Smith或来自伦敦的用户
db.users.find({
  $or: [
    { name: "Alice Smith" },
    { city: "London" }
  ]
});

常用查询操作符:

  • $eq:等于(如果未指定操作符,则为默认值)
  • $ne:不等于
  • $gt:大于
  • $gte:大于或等于
  • $lt:小于
  • $lte:小于或等于
  • $in:匹配数组中指定的任何值
  • $nin:不匹配数组中指定的任何值
  • $and$or$not$nor:逻辑操作符

c. 投影:选择特定字段

要仅返回字段的子集,将投影文档作为第二个参数传递给find()。值为1表示包含该字段,0表示排除。除非明确排除,否则默认包含_id字段。

// 仅返回姓名和电子邮件,排除_id
db.users.find({ city: "New York" }, { name: 1, email: 1, _id: 0 });

d. 排序、限制和跳过

链式方法允许更复杂的查询:

// 按年龄降序排序(1为升序,-1为降序)
db.users.find().sort({ age: -1 });

// 将结果限制为2个文档
db.users.find().limit(2);

// 跳过前2个文档并返回其余部分
db.users.find().skip(2);

// 组合操作:查找来自纽约的用户,按年龄排序,跳过1个,限制1个
db.users.find({ city: "New York" }).sort({ age: 1 }).skip(1).limit(1);

2. db.collection.findOne()

此方法返回匹配查询条件的单个文档。如果多个文档匹配,则返回找到的第一个。

db.users.findOne({ name: "Alice Smith" });

更新操作

更新操作修改集合中的现有文档。你可以更新单个文档、多个文档,甚至替换整个文档。

1. db.collection.updateOne()

更新匹配过滤条件的单个文档。

// 将Alice的城市更新为'San Francisco'
db.users.updateOne(
  { name: "Alice Smith" },
  { $set: { city: "San Francisco", lastUpdated: new Date() } }
);

2. db.collection.updateMany()

更新所有匹配过滤条件的文档。

// 将所有纽约用户的年龄增加1
db.users.updateMany(
  { city: "New York" },
  { $inc: { age: 1 } }
);

// 向尚未拥有'reading'兴趣的用户添加该兴趣
db.users.updateMany(
  {}, // 应用于所有文档
  { $addToSet: { interests: "reading" } }
);

// 从Bob Johnson的兴趣中移除'gaming'
db.users.updateOne(
  { name: "Bob Johnson" },
  { $pull: { interests: "gaming" } }
);

常用更新操作符:

  • $set:设置文档中字段的值。如果字段不存在则创建。
  • $inc:按指定数量增加字段的值。
  • $unset:从文档中移除字段。
  • $push:向数组字段追加一个值。
  • $pull:从数组中移除所有匹配指定查询的值或值的实例。
  • $addToSet:仅当值不存在时才向数组添加值。

3. db.collection.replaceOne()

用新文档替换匹配过滤条件的单个文档。被替换文档的_id字段不能更改。

// 完全替换Bob Johnson的文档
db.users.replaceOne(
  { name: "Bob Johnson" },
  {
    name: "Robert Johnson",
    occupation: "Software Engineer",
    status: "active",
    email: "[email protected]"
  }
);

Upsert选项

updateOne()updateMany()都支持upsert选项。如果设置为true,并且没有文档匹配过滤条件,MongoDB将根据查询和更新操作插入一个新文档。

db.users.updateOne(
  { name: "David Lee" },
  { $set: { age: 28, city: "Seattle" } },
  { upsert: true } // 如果David Lee不存在,则创建他
);

删除操作

删除操作从集合中移除文档。对删除操作要格外小心,因为它们是不可逆的。

1. db.collection.deleteOne()

删除最多一个匹配指定过滤条件的文档。

// 删除名为'Robert Johnson'的用户(之前是'Bob Johnson')
db.users.deleteOne({ name: "Robert Johnson" });

2. db.collection.deleteMany()

删除所有匹配指定过滤条件的文档。

// 删除所有来自伦敦的用户
db.users.deleteMany({ city: "London" });

警告:要删除集合中的所有文档,请使用空过滤器{}。此操作无法撤销,请务必谨慎:

db.users.deleteMany({}); // 删除'users'集合中的所有文档

3. db.collection.drop()

此方法从数据库中永久移除整个集合,包括其所有文档和索引。

db.users.drop(); // 删除整个'users'集合

警告:删除集合是一种高度破坏性的操作。在执行此命令之前,请确保有适当的备份或绝对确定。

MongoDB CRUD的最佳实践

  • 索引:对于频繁查询的字段,创建索引以显著加快读取操作。db.collection.createIndex({ fieldName: 1 })
  • 投影:只检索你需要的数据。使用投影({ field: 1 })可以减少网络带宽和内存使用。
  • 批量操作:在插入、更新或删除多个文档时,使用insertMany()updateMany()deleteMany()而不是单个操作,以减少开销。
  • 理解操作符:熟悉MongoDB丰富的查询和更新操作符。它们提供了强大的数据操作方式。
  • 错误处理:在生产应用程序中,始终为数据库操作实现健壮的错误处理。
  • 模式设计:虽然MongoDB是无模式的,但深思熟虑的模式设计对于高效查询和数据一致性至关重要。

在真实数据库中更安全地练习CRUD

MongoDB CRUD的危险之处不在于语法。而是在错误的数据库中运行宽泛的过滤器,尤其是使用updateMany()deleteMany()时。我喜欢对任何影响现有数据的写入操作采用三步习惯。

首先,将过滤器作为读取运行:

db.users.find(
  { city: "New York", status: "inactive" },
  { name: 1, email: 1, city: 1, status: 1 }
).limit(20);

如果预览返回了你未预期的文档,请立即停止。在考虑更新之前修复过滤器。如果返回空,请确保你处于正确的数据库中(使用db.getName()),并且字段名称与实际文档匹配。

其次,计算匹配文档的数量:

db.users.countDocuments({ city: "New York", status: "inactive" });

这可以让你大致了解影响范围。如果你预期有12个用户,但计数显示12,000,则命令表明出了问题。计数不能替代备份,但它是一个廉价的护栏。

第三,使用最适合工作的最窄命令运行写入。当唯一电子邮件、账户ID或_id应匹配一个文档时,使用updateOne()。仅当有意更改整个段时,才使用updateMany()

db.users.updateMany(
  { city: "New York", status: "inactive" },
  {
    $set: {
      marketingEmailEnabled: false,
      updatedBy: "ops-maintenance-2025-11-04"
    }
  }
);

添加updatedByupdatedAt或维护注释并非MongoDB所要求,但它有助于日后有人询问字段为何更改时提供线索。

导致实际错误的常见错误

第一个错误是当你打算更新字段时却替换了文档。这个命令看起来无害,但它会用仅显示的字段替换整个匹配文档:

db.users.updateOne(
  { email: "[email protected]" },
  { city: "Boston" }
);

现代MongoDB期望更新文档使用更新操作符,除非你使用替换样式的方法,因此此模式可能因命令和版本而异而失败。更安全的心智模型很简单:如果你要就地更改字段,请使用$set$unset$inc$push$pull或其他更新操作符。

db.users.updateOne(
  { email: "[email protected]" },
  { $set: { city: "Boston" } }
);

第二个错误是查询数组时假设顺序总是重要。{ interests: ["reading", "hiking"] }精确匹配数组。{ interests: "reading" }匹配数组包含该值的文档。{ interests: { $all: ["reading", "hiking"] } }匹配包含这两个值的数组,无论顺序如何。这是三个不同的问题。

第三个错误是当过滤器不唯一时,假设findOne()返回“正确的那个”。如果你运行:

db.users.findOne({ city: "New York" });

MongoDB返回一个匹配的文档,但如果没有排序,你不应将该文档视为最新、最旧、最重要或最具代表性。如果顺序重要,请明确说明:

db.users.find({ city: "New York" }).sort({ createdAt: -1 }).limit(1);

第四个错误是直到应用程序变慢才跳过索引。一个包含几千个文档的集合可以隐藏低效的查询。当集合增长时,相同的查询可能会变得痛苦。如果应用程序经常通过email查找用户,请在数据模型允许时创建唯一索引:

db.users.createIndex({ email: 1 }, { unique: true });

这既保护了性能,也保护了数据质量。如果已存在重复电子邮件,命令将失败,这正是你在依赖电子邮件作为标识符之前希望发现的问题。

检查写入操作的实际效果

MongoDB的写入结果值得一读。更新后,查看matchedCountmodifiedCountmatchedCount告诉你有多少文档匹配了过滤器。modifiedCount告诉你实际更改了多少。

如果matchedCount1modifiedCount0,命令可能仍然没问题。也许该字段已经具有请求的值。如果matchedCount0,则你的过滤器未匹配任何内容。当_id作为字符串而不是ObjectId传递时,这种情况很常见。

db.users.findOne({ _id: ObjectId("6650f1e59d0a41a37c2d8011") });

对于删除,检查deletedCount。如果你预期删除一个文档,但结果显示deletedCount: 0,不要立即运行更广泛的删除。重新检查数据库、集合和过滤器。

当CRUD不够用时

基本的CRUD命令涵盖了大多数日常数据工作,但某些任务需要更强大的工具。如果你要更新多个必须保持一致的集合,请查看副本集或分片集群上的事务。如果你要跨多个文档重塑数据,聚合管道可能比一系列客户端循环更清晰。如果你要迁移生产数据,请使用带有日志记录、试运行模式、备份和回滚计划的脚本。

对于mongosh中的一次性操作,保持命令可读。巧妙的一行代码更难审查,也更难恢复。在生产环境中,无聊的命令通常更好。

一旦你习惯了文档、过滤器和更新操作符,MongoDB CRUD命令就很简单了。在实际工作中重要的技能是深思熟虑:预览过滤器、计算影响、选择最窄的写入命令、读取结果,并留下足够的上下文,以便下一个人能够理解发生了什么变化。