PAYATHON 2026

MongoDB 如何确保仅存在一个文档,并在不存在时自动创建?

技术老齐

可以为首页配置约定一个固定的业务标识,用 upsert 在文档不存在时创建。如果还需要数据库拒绝第二份文档,可以用集合校验规则将 _id 限定为固定值,再配合 _id 的唯一索引,将记录数量限制为最多一条。

这两个要求分别由数据库和应用实现:

  • 数据库约束保证“最多一个文档”。
  • 应用通过初始化操作实现“不存在时创建”。文档被删除后,MongoDB 不会自行补建,需要再次执行初始化逻辑。

用固定业务标识定位文档

MongoDB 的 _id 可以使用字符串,不必使用自动生成的 ObjectId。例如,首页配置统一使用:

{ _id: "homepage" }

staging 和 production 都可以按 "homepage" 查询各自数据库中的配置,无须手动创建文档,也无须同步随机生成的 id。

代码通过这个预先定义的业务标识确定要读取哪份配置,不依赖某个环境中已经存在的记录。

但代码中的约定无法限制整个集合的文档数量。其他写入方仍可能使用不同的 _id 插入文档,因此还需要集合校验规则来强制执行限制。

在数据库中限制最多一个文档

以下示例使用 mongosh,适用于普通、未分片集合。示例集合名为 home_page_data,使用时请替换成项目中的实际集合名。

新集合可以这样创建:

db.createCollection("home_page_data", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["_id"],
      properties: {
        _id: {
          bsonType: "string",
          enum: ["homepage"]
        }
      }
    }
  },
  validationLevel: "strict",
  validationAction: "error"
});

这两项约束共同限制文档数量:

  • 集合校验规则要求文档的 _id 必须是字符串 "homepage"。
  • _id 唯一索引禁止两份文档使用相同的 _id。

所有合法文档都只能使用同一个 _id,因此集合最多只能保存一份文档。

如果集合已经存在,使用 collMod 配置校验规则:

db.runCommand({
  collMod: "home_page_data",
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["_id"],
      properties: {
        _id: {
          bsonType: "string",
          enum: ["homepage"]
        }
      }
    }
  },
  validationLevel: "strict",
  validationAction: "error"
});

修改校验规则不会自动清理已有数据,应先检查并整理现有记录。旧记录的 _id 不能直接更新,如果要改成 "homepage",需要迁移数据。

用 upsert 在不存在时创建

集合准备好后,在应用启动或部署初始化时执行:

db.home_page_data.updateOne(
  { _id: "homepage" },
  {
    $setOnInsert: {
      title: "",
      content: ""
    }
  },
  { upsert: true }
);

title 和 content 是示例字段,使用时应替换成首页配置的实际字段。

执行后会有两种结果:

  • 找到文档时,保留已有内容。
  • 未找到文档时,插入 _id 为 "homepage" 的文档,并写入默认字段。

$setOnInsert 只在插入时生效,重复执行初始化不会覆盖管理员已经保存的配置。

首页读取配置时使用:

db.home_page_data.findOne({ _id: "homepage" });

管理员修改配置时使用:

db.home_page_data.updateOne(
  { _id: "homepage" },
  {
    $set: {
      title: "首页标题",
      content: "首页内容"
    }
  }
);

如果希望管理员第一次保存时就能创建配置,可以为这次更新添加 { upsert: true }。

重复创建与并发时会怎样

文档已经存在时,执行下面的普通插入会触发重复键错误,错误码为 11000,通常显示为 E11000 duplicate key error:

db.home_page_data.insertOne({
  _id: "homepage",
  title: "另一份配置"
});

使用其他 _id 插入时,会因不符合校验规则而失败:

db.home_page_data.insertOne({
  _id: "another-homepage",
  title: "另一份配置"
});

省略 _id 时,自动生成的 ObjectId 也无法通过上述规则。

自动初始化需要允许重复调用:已有文档时,upsert 会匹配并成功返回。如果接口要求“重复创建时报错”,则应使用普通插入。

不要先用 findOne() 或 countDocuments() 判断文档是否存在,再决定是否插入,因为并发请求可能同时判断为不存在。应使用固定 _id 的 upsert,由唯一索引防止重复。如果并发初始化时收到针对该 _id 的重复键错误,可以重新读取文档;其他错误仍应正常处理。

使用时还要注意什么

建议在部署流程、应用启动阶段或管理员首次保存时执行初始化,首页读取接口只负责查询。初始化失败时,应根据应用需要重试,或阻止服务进入可用状态。

使用 Mongoose 时,需要将 _id 显式声明为 String,并确认模型映射到了正确的集合。在 Schema 中设置默认值只能约束相应的应用写入路径,要在数据库层面严格限制文档数量,仍需上述集合校验规则和唯一索引。

这些约束只保证“最多一个文档”,仍然允许删除。应根据业务需要限制删除权限,并避免让普通应用账号拥有绕过文档校验的权限。

备注:内容仅供参考。