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 中设置默认值只能约束相应的应用写入路径,要在数据库层面严格限制文档数量,仍需上述集合校验规则和唯一索引。
这些约束只保证“最多一个文档”,仍然允许删除。应根据业务需要限制删除权限,并避免让普通应用账号拥有绕过文档校验的权限。
备注:内容仅供参考。
