Mongoose如何实现可空且元素全局唯一的列表属性并修复异常?
在Mongoose中定义可空且元素全局唯一的列表属性
问题需求
需要实现一个Mongoose列表属性,满足:
- 列表中每个元素的
foo值在整个集合内唯一(跨不同文档) - 允许集合中存在多个空列表
- 同一列表内不能出现重复的
foo值
当前代码实现
const innerItemSchema = new Schema({ foo: { type: "string", required: true, unique: true, }, }); const outerItemDefinition = { list: { type: [innerItemSchema], required: true }, } as const; export type RawOuterItem = InferRawDocType<typeof outerItemDefinition> & { _id?: Types.ObjectId }; const outerItemSchema = new Schema(outerItemDefinition); export const OuterItem = model("OuterItem", outerItemSchema);
当前存在的问题
- 跨文档重复
foo值的检查正常,但插入多个空列表时会触发E11000 duplicate key error(因为空列表会被索引识别为list.foo: null,唯一索引不允许多个null) - 同一列表内的重复
foo值不会触发错误(子文档的unique约束仅作用于跨文档,不检查同文档内的数组元素)
预期行为对比
[{ list: [{foo: "a"}, {foo: "a"}] }] // 应因列表内重复foo报错 → 当前未报错 [{ list: [{foo: "a"}] }, { list: [{foo: "a"}] }] // 应因跨文档重复foo报错 → 当前行为正确 [{ list: [{foo: "a"}, {foo: "b"}] }] // 正常插入 → 当前行为正确 [{ list: [{foo: "a"}] }, { list: [{foo: "b"}] }] // 正常插入 → 当前行为正确 [{ list: [] }] // 正常插入 → 当前行为正确 [{ list: [] }, { list: [] }] // 应正常插入 → 当前触发重复键错误
解决方案
1. 替换全局唯一索引为稀疏复合索引
移除子文档innerItemSchema中的unique约束,改为在集合级别定义稀疏唯一索引,这样空列表会被索引忽略,不会触发重复键错误:
// 移除innerItemSchema中的unique: true const innerItemSchema = new Schema({ foo: { type: "string", required: true }, }); // 定义outerItemSchema后添加稀疏索引 const outerItemSchema = new Schema(outerItemDefinition); // 稀疏索引:仅包含有list.foo值的文档,空列表不参与索引 outerItemSchema.index({ 'list.foo': 1 }, { unique: true, sparse: true });
2. 添加列表内重复值验证
在list字段上添加Mongoose自定义验证器,检查同一列表内的foo值是否重复:
const outerItemDefinition = { list: { type: [innerItemSchema], required: true, validate: { validator: function(v) { // 用Set去重,对比原数组长度判断是否有重复 const fooSet = new Set(v.map(item => item.foo)); return v.length === fooSet.size; }, message: doc => `列表内存在重复的foo值: ${doc.list.map(item => item.foo).join(', ')}` } }, } as const;
完整修改后的代码
const innerItemSchema = new Schema({ foo: { type: "string", required: true }, }); const outerItemDefinition = { list: { type: [innerItemSchema], required: true, validate: { validator: function(v) { const fooSet = new Set(v.map(item => item.foo)); return v.length === fooSet.size; }, message: doc => `列表内存在重复的foo值: ${doc.list.map(item => item.foo).join(', ')}` } }, } as const; export type RawOuterItem = InferRawDocType<typeof outerItemDefinition> & { _id?: Types.ObjectId }; const outerItemSchema = new Schema(outerItemDefinition); // 添加稀疏唯一索引,实现跨文档foo唯一,同时允许多个空列表 outerItemSchema.index({ 'list.foo': 1 }, { unique: true, sparse: true }); export const OuterItem = model("OuterItem", outerItemSchema);
验证修改后的行为
- 同一列表内重复
foo值:触发验证错误,无法插入 - 跨文档重复
foo值:触发唯一索引错误,无法插入 - 单个空列表:正常插入
- 多个空列表:正常插入(稀疏索引忽略空列表,不会触发重复键错误)
- 不同文档的不同
foo值:正常插入
内容的提问来源于stack exchange,提问作者nick zoum
相关产品推荐
相关产品推荐

