如何在Mongoose Schema中限制仅存在一个管理员角色?
实现单管理员唯一性限制的最优方案
要确保系统同一时间只有一个管理员,自定义验证器是可行思路,但更推荐从数据库层面做约束,以下是几种方案的实现和对比:
1. 数据库条件唯一索引(最推荐)
直接在MongoDB中创建带过滤条件的唯一索引,从根源上避免数据不一致,完全解决并发写入的竞态问题:
// 在schema定义完成后添加索引配置 userSchema.index({ role: 1 }, { unique: true, partialFilterExpression: { role: "admin" } });
- 优势:数据库层面强制生效,不管是通过Mongoose操作还是直接用MongoDB客户端修改数据,都能保证最多只有一个admin用户,彻底杜绝并发场景下的冲突。
- 注意事项:创建索引前要先清理现有数据,确保当前数据库里最多只有一个admin,否则索引创建会失败。
2. 自定义验证器
适合并发场景少的小型系统,但存在并发风险:
const userSchema = new mongoose.Schema({ role: { type: String, enum: ["user", "admin"], default: "user", validate: { validator: async function(roleValue) { if (roleValue !== "admin") return true; let adminCount = await this.constructor.countDocuments({ role: "admin" }); // 如果是更新操作,排除当前文档本身 if (this._id) { const currentDoc = await this.constructor.findById(this._id); if (currentDoc?.role === "admin") { adminCount -= 1; } } // 新增admin时必须没有现有admin,更新时确保总数不超过0(减去自身后) return adminCount === 0; }, message: "系统最多只能有一个管理员" } }, });
- 劣势:高并发场景下可能出现两个请求同时验证admin数量为0,然后都写入成功,导致出现两个管理员。
3. Mongoose中间件(pre-save/pre-update)
在数据保存或更新前做校验,同样存在并发缺陷:
// 处理新增和更新操作的中间件 userSchema.pre(['save', 'findOneAndUpdate'], async function(next) { let targetRole; // 区分Query操作(比如findOneAndUpdate)和Document操作(save) if (this instanceof mongoose.Query) { targetRole = this.getUpdate().role; } else { targetRole = this.role; } if (targetRole !== "admin") return next(); const adminCount = await this.model.countDocuments({ role: "admin" }); // 处理更新时排除当前文档的情况 if (this instanceof mongoose.Query) { const existingDoc = await this.model.findOne(this.getQuery()); if (existingDoc?.role === "admin") { // 如果当前文档本来就是admin,修改其他字段不影响;如果是把普通用户改成admin,要检查总数 if (existingDoc.role !== targetRole && adminCount >= 1) { return next(new Error("系统最多只能有一个管理员")); } } else if (adminCount >= 1) { return next(new Error("系统最多只能有一个管理员")); } } else { // 保存操作:新增admin或者普通用户升级为admin if ((this.isNew && targetRole === "admin") || (this.role !== this.$original.role && targetRole === "admin")) { if (adminCount >= 1) { return next(new Error("系统最多只能有一个管理员")); } } } next(); });
- 劣势:和自定义验证器一样,无法应对高并发场景下的竞态问题,不如数据库索引可靠。
总结
优先选择数据库条件唯一索引,这是最稳妥的方案,从底层保证数据一致性,避免业务逻辑层面的各种漏洞。
内容的提问来源于stack exchange,提问作者Brenden Baio
相关产品推荐
相关产品推荐

