Skip to content

Latest commit

 

History

History
595 lines (499 loc) · 13.3 KB

File metadata and controls

595 lines (499 loc) · 13.3 KB

findOneAndUpdate() - 原子查找并更新

📑 目录


原子地查找并更新单个文档,返回更新前或更新后的文档。这是一个原子操作,适合需要读取旧值同时更新的场景。

语法

collection(collectionName).findOneAndUpdate(filter, update, options)

参数

filter (Object, 必需)

筛选条件。

update (Object, 必需)

更新操作,必须使用更新操作符。

options (Object, 可选)

选项 类型 默认值 说明
projection Object - 字段投影
sort Object - 排序条件
upsert Boolean false 不存在时是否插入
returnDocument String "before" "before" 或 "after"
maxTimeMS Number - 最大执行时间
comment String - 操作注释
collation Object - 排序规则
arrayFilters Array - 数组过滤器
hint String/Object - 索引提示
includeResultMetadata Boolean false 是否包含完整元数据

返回值

默认返回文档对象null(未找到)。

如果 includeResultMetadata: true,返回:

{
  value: <文档或null>,
  ok: 1,
  lastErrorObject: {
    updatedExisting: true,
    n: 1,
    upserted: <id>  // 仅 upsert 时
  }
}

⚠️ 重要提示 - MongoDB 驱动 6.x 兼容性

monSQLize 使用 MongoDB Node.js 驱动 6.x,该版本对 findOneAndUpdate 的返回值格式进行了重要变更:

驱动 6.x (当前版本):

  • 默认直接返回文档对象
  • 需要显式设置 includeResultMetadata: true 才返回完整元数据

驱动 5.x 及更早版本:

  • 默认返回完整元数据 { value, ok, lastErrorObject }

✅ monSQLize 的处理:

  • 已在内部自动处理此差异,用户无需关心驱动版本
  • API 行为保持一致,向后兼容
  • 详见技术分析报告: analysis-reports/2025-11-17-mongodb-driver-6x-compatibility.md

核心特性

原子性保证

// ✅ 原子操作 - 查找和更新在同一事务中
const oldDoc = await collection("counters").findOneAndUpdate(
  { counterName: "orderNumber" },
  { $inc: { value: 1 } }
);

// ❌ 非原子 - 有竞态条件风险
const doc = await collection("counters").findOne({ counterName: "orderNumber" });
await collection("counters").updateOne(
  { counterName: "orderNumber" },
  { $inc: { value: 1 } }
);

returnDocument 选项

// 返回更新前的文档(默认)
const oldDoc = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  { $inc: { loginCount: 1 } }
);
console.log("Old count:", oldDoc.loginCount); // 5

// 返回更新后的文档
const newDoc = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  { $inc: { loginCount: 1 } },
  { returnDocument: "after" }
);
console.log("New count:", newDoc.loginCount); // 6

常见场景

场景 1: 分布式计数器

// 原子递增并获取新值
const counter = await collection("counters").findOneAndUpdate(
  { counterName: "orderNumber" },
  { $inc: { value: 1 } },
  { returnDocument: "after" }
);

const newOrderNumber = counter.value; // 1001
console.log(`New order number: ${newOrderNumber}`);

场景 2: 乐观锁(版本控制)

// 使用版本号防止并发冲突
const doc = await collection("documents").findOneAndUpdate(
  {
    docId: "doc1",
    version: 5  // 仅当版本号匹配时更新
  },
  {
    $set: { content: "Updated content" },
    $inc: { version: 1 }
  },
  { returnDocument: "after" }
);

if (!doc) {
  console.log("更新失败:版本冲突");
} else {
  console.log("更新成功,新版本:", doc.version);
}

场景 3: 任务队列

// 原子地获取并标记任务
const task = await collection("tasks").findOneAndUpdate(
  { status: "pending" },
  {
    $set: {
      status: "processing",
      workerId: "worker-1",
      startedAt: new Date()
    }
  },
  {
    sort: { priority: -1 },  // 优先级最高的
    returnDocument: "after"
  }
);

if (task) {
  console.log("Processing task:", task.taskId);
  // 处理任务...
} else {
  console.log("No pending tasks");
}

场景 4: 分布式锁

// 获取锁
const lock = await collection("locks").findOneAndUpdate(
  {
    lockKey: "resource-lock",
    locked: false
  },
  {
    $set: {
      locked: true,
      ownerId: "worker-1",
      acquiredAt: new Date()
    }
  },
  { returnDocument: "after" }
);

if (lock) {
  try {
    // 执行需要锁保护的操作...
    console.log("Lock acquired");
  } finally {
    // 释放锁
    await collection("locks").updateOne(
      { lockKey: "resource-lock" },
      { $set: { locked: false } }
    );
  }
} else {
  console.log("Lock already held");
}

场景 5: 用户最后活动时间

// 更新活动时间并返回用户信息
const user = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  {
    $set: { lastActiveAt: new Date() },
    $inc: { pageViews: 1 }
  },
  {
    projection: { name: 1, email: 1, lastActiveAt: 1 },
    returnDocument: "after"
  }
);

console.log(`Welcome back, ${user.name}!`);

示例

基本用法

const oldDoc = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  { $set: { status: "active" } }
);

if (oldDoc) {
  console.log("Old status:", oldDoc.status);
} else {
  console.log("User not found");
}

使用排序

// 找到分数最高的用户并更新
const topUser = await collection("users").findOneAndUpdate(
  { status: "active" },
  { $set: { winner: true } },
  {
    sort: { score: -1 },
    returnDocument: "after"
  }
);

使用投影

// 只返回需要的字段
const user = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  { $inc: { loginCount: 1 } },
  {
    projection: { name: 1, loginCount: 1 },
    returnDocument: "after"
  }
);
// user 只包含 _id, name, loginCount

使用 upsert(不存在就插入,存在则更新)⭐

// 基本用法:计数器示例
const counter = await collection("counters").findOneAndUpdate(
  { counterName: "pageViews" },
  { $inc: { value: 1 } },
  {
    upsert: true,
    returnDocument: "after"
  }
);
// 如果不存在会创建新文档

Upsert 详细说明

upsert = update + insert 的组合:

  • 存在:执行更新操作
  • 不存在:插入新文档

使用场景

// 场景 1:用户配置(不存在则创建默认配置)
const userConfig = await collection("user_configs").findOneAndUpdate(
  { userId: "user123" },
  {
    $set: {
      theme: "dark",
      language: "zh-CN",
      updatedAt: new Date()
    },
    $setOnInsert: {
      // 仅在插入时设置
      createdAt: new Date(),
      defaultSettings: true
    }
  },
  {
    upsert: true,
    returnDocument: "after"
  }
);

// 场景 2:统计数据(自动初始化)
const stats = await collection("daily_stats").findOneAndUpdate(
  {
    date: "2026-01-28",
    userId: "user123"
  },
  {
    $inc: { pageViews: 1, loginCount: 1 }
  },
  {
    upsert: true,
    returnDocument: "after"
  }
);
// 不存在时会创建:{ date: "2026-01-28", userId: "user123", pageViews: 1, loginCount: 1 }

// 场景 3:缓存更新(不存在则缓存新数据)
const cache = await collection("cache").findOneAndUpdate(
  { key: "user:profile:123" },
  {
    $set: {
      value: profileData,
      expireAt: new Date(Date.now() + 3600000) // 1小时后过期
    }
  },
  {
    upsert: true,
    returnDocument: "after"
  }
);

// 场景 4:商品库存(自动创建库存记录)
const inventory = await collection("inventory").findOneAndUpdate(
  { productId: "prod-456" },
  {
    $inc: { quantity: -1 },  // 减少库存
    $set: { lastUpdated: new Date() }
  },
  {
    upsert: true,
    returnDocument: "after"
  }
);

⚠️ Upsert 注意事项

// ❌ 错误:使用 $setOnInsert 但忘记 upsert
const doc = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  { $setOnInsert: { createdAt: new Date() } }
  // 缺少 upsert: true,$setOnInsert 不会生效
);

// ✅ 正确:同时使用 $set 和 $setOnInsert
const doc = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  {
    $set: { lastLogin: new Date() },        // 每次都更新
    $setOnInsert: { createdAt: new Date() } // 仅插入时设置
  },
  { upsert: true }
);

// ✅ 正确:获取 upsert 的 _id
const result = await collection("users").findOneAndUpdate(
  { email: "new@example.com" },
  { $set: { name: "New User" } },
  {
    upsert: true,
    returnDocument: "after",
    includeResultMetadata: true
  }
);

if (result.lastErrorObject.upserted) {
  console.log("创建了新文档,_id:", result.lastErrorObject.upserted);
} else {
  console.log("更新了现有文档");
}

获取完整元数据

const result = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  { $set: { status: "active" } },
  { includeResultMetadata: true }
);

console.log("Document:", result.value);
console.log("Updated existing:", result.lastErrorObject.updatedExisting);
console.log("Operation ok:", result.ok);

性能优化

1. 使用索引

// ✅ 推荐 - 在筛选字段上建立索引
await collection("counters").findOneAndUpdate(
  { counterName: "orderNumber" }, // counterName 应有索引
  { $inc: { value: 1 } }
);

2. 使用投影减少数据传输

// ✅ 推荐 - 只返回需要的字段
const user = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  { $inc: { score: 10 } },
  {
    projection: { _id: 0, score: 1 },
    returnDocument: "after"
  }
);

3. 配合 sort 和 hint 优化查询

const task = await collection("tasks").findOneAndUpdate(
  { status: "pending" },
  { $set: { status: "processing" } },
  {
    sort: { priority: -1, createdAt: 1 },
    hint: "status_priority_createdAt_idx", // 使用复合索引
    returnDocument: "after"
  }
);

错误处理

try {
  const doc = await collection("users").findOneAndUpdate(
    { userId: "user123" },
    { $inc: { score: 10 } }
  );

  if (!doc) {
    console.log("Document not found");
  }
} catch (err) {
  if (err.code === "INVALID_ARGUMENT") {
    console.error("参数错误:", err.message);
  } else if (err.code === "DUPLICATE_KEY") {
    console.error("唯一性约束冲突:", err.message);
  } else {
    console.error("操作失败:", err);
  }
}

与其他方法的对比

方法 原子性 返回值 场景
updateOne 计数 普通更新
findOneAndUpdate 文档 需要旧值/原子操作
findOne + updateOne 文档+计数 ⚠️ 有竞态风险

并发安全

安全示例

// ✅ 安全 - 原子操作
for (let i = 0; i < 10; i++) {
  await collection("counters").findOneAndUpdate(
    { name: "total" },
    { $inc: { value: 1 } }
  );
}
// 最终 value = 10(正确)

// ❌ 不安全 - 非原子操作
for (let i = 0; i < 10; i++) {
  const doc = await collection("counters").findOne({ name: "total" });
  await collection("counters").updateOne(
    { name: "total" },
    { $set: { value: doc.value + 1 } }
  );
}
// 最终 value 可能 < 10(错误,有竞态条件)

最佳实践

1. 合理选择 returnDocument

// 需要旧值时
const oldValue = await collection("counters").findOneAndUpdate(
  { name: "counter" },
  { $inc: { value: 1 } }
  // returnDocument: "before" 是默认值
);

// 需要新值时
const newValue = await collection("counters").findOneAndUpdate(
  { name: "counter" },
  { $inc: { value: 1 } },
  { returnDocument: "after" }
);

2. 使用版本号避免冲突

async function updateWithRetry(docId, newContent, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const doc = await collection("documents").findOne({ docId });

    const result = await collection("documents").findOneAndUpdate(
      { docId, version: doc.version },
      {
        $set: { content: newContent },
        $inc: { version: 1 }
      },
      { returnDocument: "after" }
    );

    if (result) return result;

    console.log(`Retry ${i + 1}: version conflict`);
  }

  throw new Error("Update failed after retries");
}

3. 使用 projection 优化性能

// ✅ 推荐
const user = await collection("users").findOneAndUpdate(
  { userId: "user123" },
  { $inc: { score: 10 } },
  {
    projection: { score: 1 },
    returnDocument: "after"
  }
);

相关方法

参考资料