Skip to content

MongoDB CRUD:增删改查的正确姿势

你已经学会了怎么连接 MongoDB,现在该学学怎么操作数据了。

CRUD——Create(创建)、Read(读取)、Update(更新)、Delete(删除),是所有数据库操作的基础。

但 MongoDB 的 CRUD 和 MySQL 有些不一样,最大的区别在于:你不是在操作行,而是在操作文档


插入(Create)

插入单篇文档

java
MongoCollection<Document> collection = database.getCollection("users");

Document user = new Document()
    .append("username", "zhangsan")
    .append("age", 28)
    .append("email", "zhangsan@example.com")
    .append("tags", List.of("Java", "MongoDB"));

// 插入单篇
InsertOneResult result = collection.insertOne(user);
System.out.println("插入的文档ID: " + result.getInsertedId());

批量插入

java
List<Document> users = List.of(
    new Document().append("username", "user1").append("age", 20),
    new Document().append("username", "user2").append("age", 25),
    new Document().append("username", "user3").append("age", 30)
);

InsertManyResult result = collection.insertMany(users);
System.out.println("插入了 " + result.getInsertedIds().size() + " 篇文档");

插入或更新(upsert)

如果文档不存在就插入,存在就更新:

java
collection.updateOne(
    eq("username", "zhangsan"),  // 查询条件
    new Document("$set", new Document("age", 29)),  // 更新操作
    new UpdateOptions().upsert(true)  // 启用 upsert
);

查询(Read)

基础查询

java
import static com.mongodb.client.model.Filters.*;
import static com.mongodb.client.model.Sorts.*;
import static com.mongodb.client.model.Projections.*;

// 查询单个文档
Document user = collection.find(eq("username", "zhangsan")).first();

// 查询多个文档
List<Document> adults = collection.find(gt("age", 18)).into(new ArrayList<>());

// 带排序
List<Document> sortedUsers = collection
    .find()
    .sort(descending("age"))
    .into(new ArrayList<>());

// 分页查询(每页10条,第2页)
List<Document> page2 = collection
    .find()
    .sort(descending("createdAt"))
    .skip(10)
    .limit(10)
    .into(new ArrayList<>());

常用查询条件

操作MongoDB说明
等于eq("age", 25)age = 25
大于gt("age", 25)age > 25
小于lt("age", 25)age < 25
大于等于gte("age", 25)age >= 25
包含in("status", List.of("active", "pending"))status in (...)
正则regex("username", "^zhang")username like 'zhang%'
and(eq("age", 25), eq("city", "Beijing"))age = 25 AND city = 'Beijing'
or(eq("city", "Beijing"), eq("city", "Shanghai"))city = 'Beijing' OR city = 'Shanghai'

嵌套字段查询

java
// 查询嵌入文档的字段
// 数据: { "profile": { "age": 28 } }
Document user = collection.find(eq("profile.age", 28)).first();

// 查询嵌套数组
// 数据: { "tags": ["Java", "MongoDB"] }
List&lt;Document&gt; javaUsers = collection.find(eq("tags", "Java")).into(new ArrayList&lt;&gt;());

只返回需要的字段

java
// 只返回 username 和 email,不返回 _id
List&lt;Document&gt; users = collection
    .find()
    .projection(fields(include("username", "email"), excludeId()))
    .into(new ArrayList&lt;&gt;());

更新(Update)

基础更新操作符

操作符说明示例
$set设置字段值set("age", 30)
$inc递增/递减inc("views", 1)
$push添加到数组push("tags", "newTag")
$pull从数组移除pull("tags", "oldTag")
$addToSet集合添加(不重复)addToSet("tags", "newTag")
$unset删除字段unset("tempField")
$rename重命名字段rename("oldName", "newName")

示例代码

java
// $set:更新单个字段
collection.updateOne(
    eq("username", "zhangsan"),
    new Document("$set", new Document("email", "new@example.com"))
);

// $inc:递增计数器
// 数据: { "views": 100 }
collection.updateOne(
    eq("_id", articleId),
    new Document("$inc", new Document("views", 1))
);
// 结果: { "views": 101 }

// $push:添加数组元素
collection.updateOne(
    eq("username", "zhangsan"),
    new Document("$push", new Document("comments", "new comment"))
);

// $set + 嵌套字段
collection.updateOne(
    eq("username", "zhangsan"),
    new Document("$set", new Document("profile.city", "Shanghai"))
);

// $unset:删除字段
collection.updateOne(
    eq("username", "zhangsan"),
    new Document("$unset", new Document("tempField", 1))
);

批量更新

java
// 将所有 age &lt; 18 的用户标记为未成年
UpdateResult result = collection.updateMany(
    lt("age", 18),
    new Document("$set", new Document("status", "minor"))
);
System.out.println("更新了 " + result.getModifiedCount() + " 条记录");

删除(Delete)

删除单个文档

java
// 删除第一条匹配的文档
DeleteResult result = collection.deleteOne(eq("username", "zhangsan"));
System.out.println("删除了 " + result.getDeletedCount() + " 条记录");

批量删除

java
// 删除所有 age > 100 的异常数据
DeleteResult result = collection.deleteMany(gt("age", 100));
System.out.println("删除了 " + result.getDeletedCount() + " 条记录");

清空整个集合

java
// 删除所有文档,集合本身保留
collection.deleteMany(new Document());  // 空 Document 匹配所有

// 删除整个集合(包括索引)
collection.drop();

原子性与批量操作

findAndModify

原子性地查询并修改,返回修改后的文档:

java
// 抢购场景:原子性扣库存
Document result = collection.findOneAndUpdate(
    and(
        eq("product_id", productId),
        gt("stock", 0)  // 库存大于0才能扣
    ),
    new Document("$inc", new Document("stock", -1)),
    new FindOneAndUpdateOptions().returnNew(true)
);

if (result != null) {
    System.out.println("抢购成功,剩余库存: " + result.getInteger("stock"));
} else {
    System.out.println("库存不足");
}

BulkWrite 批量操作

在一次请求中执行多种操作,减少网络往返:

java
BulkWriteOperation bulk = collection.initializeOrderedBulkOperation();

// 插入
bulk.insert(new Document("username", "newuser1").append("age", 22));

// 更新
bulk.find(eq("username", "zhangsan"))
    .updateOne(new Document("$inc", new Document("age", 1)));

// 删除
bulk.find(eq("username", "todelete"))
    .remove();

BulkWriteResult result = bulk.execute();

常见错误与避坑

错误一:更新整个文档

java
// ❌ 错误:$set 整个文档,会丢失其他字段
collection.updateOne(eq("_id", id), 
    new Document("username", "newname").append("age", 30));

// ✅ 正确:只更新需要的字段
collection.updateOne(eq("_id", id),
    new Document("$set", new Document("username", "newname")));

错误二:upsert 导致重复

java
// 如果查询条件匹配不到,会插入新文档
// 但如果你以为会更新,实际上插入了新数据,可能导致数据重复
collection.updateOne(
    eq("email", "zhangsan@example.com"),  // 可能有多个相同email的记录
    new Document("$set", new Document("name", "newname")),
    new UpdateOptions().upsert(true)
);

// ✅ 更好的做法:确保查询条件唯一
collection.updateOne(
    and(eq("email", "zhangsan@example.com"), eq("status", "active")),
    new Document("$set", new Document("name", "newname")),
    new UpdateOptions().upsert(true)
);

总结

MongoDB 的 CRUD 操作比你想象的简单,比 MySQL 更灵活。记住几个关键点:

  1. 查询用 Filter,更新用 $set 等操作符
  2. 嵌套字段用点号profile.age
  3. upsert 可以让你少写很多判断逻辑
  4. findAndModify 保证原子性,适合计数器、抢购等场景

面试追问方向

  • MongoDB 的批量操作(BulkWrite)和逐条执行有什么区别?性能差异有多大?
  • 如何用 MongoDB 实现一个分布式锁?
  • $push$addToSet 的区别是什么?

基于 VitePress 构建