MongoDB底层upsert操作与replace区别详解

小豆丁 昨天 ⋅ 9 阅读
聊天记录
头像

mongo底层有insert update replace upsert? upsert 会replace吗?

头像

非常好的问题!我们来澄清 MongoDB 底层的核心操作。

MongoDB 底层核心操作

MongoDB 驱动程序(Driver Level)提供以下核心操作:

操作 方法 描述
Insert insertOne(), insertMany() 只插入新文档,如果文档已存在(_id 冲突)会报错
Update updateOne(), updateMany() 只更新匹配的文档,使用更新操作符($set, $inc 等)
Replace replaceOne() 替换整个匹配的文档(保留 _id)
Delete deleteOne(), deleteMany() 删除文档
Upsert 不是一个独立操作,是选项 update()replaceOne(){ upsert: true } 选项

关键澄清:upsert 是选项,不是独立操作

upsert 是 update()replaceOne() 的一个选项

JAVASCRIPT
// upsert 作为 update 的选项
db.collection.updateOne(
  filter,
  update,           // 更新操作符
  { upsert: true }  // 选项
)

// upsert 作为 replaceOne 的选项
db.collection.replaceOne(
  filter,
  replacement,      // 完整的新文档
  { upsert: true }  // 选项
)

问题回答:upsert 会 replace 吗?

这取决于你调用的是哪个方法

情况1:使用 update() 的 upsert → 不会 replace

JAVASCRIPT
// 这是更新操作,不是替换
db.users.updateOne(
  { _id: 1 },
  { $set: { name: "Alice" } },  // 使用更新操作符
  { upsert: true }
)

行为

  • 如果文档存在:只更新 name 字段
  • 如果文档不存在:创建新文档,包含 _id: 1name: "Alice"
  • 结果不会替换整个文档,只修改指定字段

情况2:使用 replaceOne() 的 upsert → 会 replace

JAVASCRIPT
// 这是替换操作
db.users.replaceOne(
  { _id: 1 },
  { name: "Alice" },  // 新文档(注意:不包含 _id)
  { upsert: true }
)

行为

  • 如果文档存在:用新文档完全替换(只保留 _id)
  • 如果文档不存在:插入新文档 { _id: 1, name: "Alice" }
  • 结果会替换整个文档

MongoDB 驱动层面源码示例

让我们看看 MongoDB Java 驱动的源码(简化):

JAVA
// UpdateOperation.java
public UpdateResult execute(WriteBinding binding) {
    if (hasUpdateOperators()) {
        // 使用更新操作符($set, $inc 等)
        return updateWithOperators(binding);
    } else {
        // 没有更新操作符 -> 替换操作
        return replaceDocument(binding);
    }
}

private UpdateResult updateWithOperators(WriteBinding binding) {
    BsonDocument updateDocument = getUpdateDocument();
    // 底层调用 MongoDB 的 update 命令
    return collection.updateOne(filter, updateDocument, options);
}

private UpdateResult replaceDocument(WriteBinding binding) {
    BsonDocument replacement = getReplacement();
    // 底层调用 MongoDB 的 update 命令,但带有特殊标志
    return collection.replaceOne(filter, replacement, options);
}

Spring Data MongoDB 的映射

Spring Data 将操作映射到底层 MongoDB 操作:

Spring Data 方法 MongoDB 操作 是否支持 upsert
mongoTemplate.save() replaceOne(..., { upsert: true }) 是,强制 upsert
mongoTemplate.updateFirst().upsert() updateOne(..., { upsert: true }) 是,可选
mongoTemplate.replaceOne().upsert() replaceOne(..., { upsert: true }) 是,可选
mongoTemplate.insert() insertOne() 否,纯插入

底层协议:MongoDB Wire Protocol

在协议层面,updatereplace 都是通过 OP_UPDATEOP_MSG 消息实现的,区别在于 update 字段的内容:

JAVASCRIPT
// MongoDB Wire Protocol 的 update 命令
{
  "update": "users",
  "updates": [
    {
      "q": { "_id": 1 },                     // 查询条件
      "u": { "$set": { "name": "Alice" } },  // 更新操作符
      "upsert": true,                        // upsert 选项
      "multi": false
    }
  ]
}

// MongoDB Wire Protocol 的 replace 命令
{
  "update": "users",
  "updates": [
    {
      "q": { "_id": 1 },                     // 查询条件
      "u": { "name": "Alice" },              // 完整的替换文档
      "upsert": true,                        // upsert 选项
      "multi": false
    }
  ]
}

注意:MongoDB 协议中,replaceOne 实际上也是通过 update 命令实现的,只是 u 字段包含的是完整文档而不是更新操作符。

重要区别:更新操作符 vs 完整文档

MongoDB 如何区分是更新还是替换?

JAVASCRIPT
// 判断规则:
function isUpdateDocument(doc) {
  // 如果文档的第一个键以 "$" 开头,则是更新操作符
  return Object.keys(doc)[0].startsWith("$");
}

// 示例:
{ $set: { name: "Alice" } }    // 更新操作符 -> update
{ $inc: { count: 1 } }         // 更新操作符 -> update
{ name: "Alice" }              // 普通文档 -> replace
{ _id: 1, name: "Alice" }      // 普通文档 -> replace

实际测试验证

让我们用 MongoDB Shell 验证:

JAVASCRIPT
// 1. 创建集合
db.test.drop();

// 2. 使用 update 的 upsert(不会 replace)
db.test.updateOne(
  { _id: 1 },
  { $set: { name: "Alice", age: 25 } },
  { upsert: true }
);
// 结果:{ _id: 1, name: "Alice", age: 25 }

// 3. 添加额外字段
db.test.updateOne(
  { _id: 1 },
  { $set: { city: "NY" } }
);
// 结果:{ _id: 1, name: "Alice", age: 25, city: "NY" }

// 4. 使用 replaceOne 的 upsert(会 replace)
db.test.replaceOne(
  { _id: 1 },
  { name: "Bob" },  // 注意:没有 age 和 city
  { upsert: true }
);
// 结果:{ _id: 1, name: "Bob" }  // age 和 city 被替换掉了!

在 Spring Data 中如何选择

JAVA
// 1. 使用 Update 对象 -> 更新操作
Update update = Update.update("name", "Alice").inc("count", 1);
UpdateResult result = mongoTemplate.updateFirst(
    Query.query(Criteria.where("_id").is(1)),
    update,
    Person.class
).upsert();  // 底层是 update with upsert

// 2. 使用 save() -> 替换操作
Person person = new Person(1, "Alice");
mongoTemplate.save(person);  // 底层是 replaceOne with upsert

// 3. 明确使用替换操作
Person replacement = new Person(1, "Bob");
mongoTemplate.replaceOne(
    Query.query(Criteria.where("_id").is(1)),
    replacement,
    ReplaceOptions.empty().upsert()  // 显式指定 upsert
);

总结

  1. MongoDB 底层有insertOne(), insertMany(), updateOne(), updateMany(), replaceOne(), deleteOne(), deleteMany()

  2. upsert 不是独立操作:是 update()replaceOne() 的一个选项

  3. upsert 会 replace 吗

    • 如果你用 update() 的 upsert:不会 replace,只更新指定字段
    • 如果你用 replaceOne() 的 upsert:会 replace,替换整个文档
  4. 如何区分:看是否使用更新操作符($set, $inc 等)

    • $ 操作符 → 更新操作 → 不会 replace
    • $ 操作符 → 替换操作 → 会 replace
  5. Spring Data 的映射

    • Update 对象 → 更新操作
    • save() 或完整对象 → 替换操作

简单记忆upsert 本身不决定是更新还是替换,它只是告诉 MongoDB"有就更新/替换,没有就插入"。决定更新还是替换的是你使用的操作类型。

全部评论: 0

    我有话说: