MongoDB 基础 CRUD 操作完全指南:从零开始掌握数据操作

想象一下,你已经安装好了 MongoDB,也理解了文档、集合、BSON 等核心概念,现在你面临的问题是:如何在 MongoDB 中创建、读取、更新和删除数据?

CRUD 操作是数据库应用的基石,也是开发者每天都要面对的任务。本文将带你系统掌握 MongoDB 的四大基础操作,通过丰富的实例和最佳实践,让你能够自信地进行数据操作。

今天,我们将从最简单的插入一条数据开始,逐步深入到批量操作、条件查询、复杂更新,最后掌握完整的 CRUD 操作体系。

目录

  1. 为什么 CRUD 操作如此重要?
  2. 准备工作:连接 MongoDB
  3. Create - 创建操作:插入数据
  4. Read - 读取操作:查询数据
  5. Update - 更新操作:修改数据
  6. Delete - 删除操作:移除数据
  7. 批量操作:bulkWrite()
  8. 实际应用场景
  9. 企业开发中的常见问题与解决方案
  10. 企业级 CRUD 操作解决方案
  11. 总结:从基础到企业级的完整闭环
  12. 互动与下一步

为什么 CRUD 操作如此重要?

CRUD 是数据库操作的核心

CRUD 代表四种基本的数据操作:

  • C (Create) - 创建/插入数据
  • R (Read) - 读取/查询数据
  • U (Update) - 更新/修改数据
  • D (Delete) - 删除数据

MongoDB vs 传统 SQL 对比

让我们先看看 MongoDB 和 SQL 在 CRUD 操作上的对应关系:

操作SQLMongoDB说明
创建INSERT INTOinsertOne() / insertMany()插入文档
读取SELECTfind() / findOne()查询文档
更新UPDATEupdateOne() / updateMany()更新文档
删除DELETEdeleteOne() / deleteMany()删除文档
替换REPLACE (不常用)replaceOne()完整替换文档
查找并-findOneAndUpdate()原子性查找并更新
批量INSERT … SELECTbulkWrite()批量操作
计数SELECT COUNT(*)countDocuments()统计文档数量
去重SELECT DISTINCTdistinct()获取唯一值
聚合GROUP BY, JOIN, HAVINGaggregate()复杂数据分析

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 的查询语法,学习如何编写高效的复杂查询,掌握数据检索的高级技巧。敬请期待!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值