Mongoose填充关联文档时如何排除Schema虚拟字段?
问题还原
你遇到的MongoError: Projection cannot have a mix of inclusion and exclusion错误,是MongoDB和Mongoose的规则共同导致的。先把你的场景梳理清楚:
你有两个关联的Schema:Item的owner字段关联到User,User定义了虚拟字段isPhoneVerified,尝试在populate Item的owner时不返回这个虚拟字段,但在select中同时写了包含name、排除_id和isPhoneVerified,直接触发了错误。
先贴出你的Schema方便参考:
User Schema
let userSchema = new mongoose.Schema({ name: {type: String}, email: {type: String}, phoneNumber: { type: String, required: true, trim: true }, devices: [String], verified: { phoneNumber: { type: Boolean, default: false } }, settings: { showNumber: {type: Boolean}, showEmail: {type: Boolean} }, role: { type: String, enum: constants.user.roles, required: true, default: constants.user.defaultRole } }); userSchema.virtual('isPhoneVerified').get(function () { return !!this.verified.phoneNumber; }); module.exports = mongoose.model('user', userSchema);
Item Schema
let itemSchema = new mongoose.Schema({ title: {type: String, required: true}, price: {type: Number}, owner: { type: mongoose.Schema.Types.ObjectId, ref: 'user', require: true } }); let autoPopulate = function(next) { this.populate({ path: 'owner', select: {name: 1, _id: 0, isPhoneVerified: 0} }); next(); }; itemSchema.pre('find', autoPopulate); itemSchema.pre('findOne', autoPopulate); module.exports = mongoose.model('item', itemSchema);
为什么会报错?
这里有两个核心原因:
- MongoDB投影规则限制:MongoDB不允许在投影(即select参数)中同时混合包含(如
name:1)和排除(如isPhoneVerified:0)规则,唯一例外是_id字段——你可以在包含其他字段时排除_id,但其他字段不行。你同时包含name、排除isPhoneVerified,刚好违反了这个规则。 - 虚拟字段的本质:
isPhoneVerified是Mongoose的虚拟字段,它不存在于MongoDB集合中,是Mongoose拉取文档后动态计算出来的。所以你试图在数据库层面的投影里排除它,本身就是无效操作——数据库里根本没有这个字段可以排除。
解决办法
方法1:用toJSON/toObject的transform全局控制(你已找到的方案)
这是最优雅的全局解决方案,通过给User Schema设置转换选项,在文档转为JSON或普通对象时自动移除不需要的字段:
userSchema.set('toJSON', { virtuals: true, getters: true, transform: function (doc, ret) { delete ret._id; delete ret.isPhoneVerified; return ret; } });
如果你的场景需要直接操作文档对象(而非仅返回JSON),可以同时配置toObject选项,确保两种场景都生效:
userSchema.set('toObject', { virtuals: true, getters: true, transform: function (doc, ret) { delete ret._id; delete ret.isPhoneVerified; return ret; } });
方法2:调整populate的select策略,避免混合规则
如果你不想全局修改User的输出,可以调整populate的select参数:
只包含需要的字段:明确列出所有要从User获取的字段,因为虚拟字段不在数据库中,默认不会被返回(除非你全局开启了
virtuals: true):this.populate({ path: 'owner', select: 'name -_id' // 只取name字段,排除_id(这是允许的例外) });这样返回的
owner对象里不会有isPhoneVerified,也不会触发投影错误。只排除不需要的字段:如果需要User的大部分字段,只排除少数字段,可以这样写(注意不能同时包含其他字段):
this.populate({ path: 'owner', select: '-isPhoneVerified -_id' });不过要注意,这里的
-isPhoneVerified不会对数据库查询产生影响,因为数据库里没有这个字段,还是需要结合Mongoose的转换选项或者手动删除来移除它。
方法3:查询后手动移除虚拟字段
如果只是特定查询需要移除这个字段,可以在查询结果返回后手动处理:
Item.find().exec((err, items) => { if (err) { // 处理错误逻辑 return; } items.forEach(item => { // 手动删除虚拟字段 delete item.owner.isPhoneVerified; }); // 后续结果处理 });
这种方式适合临时需求,但代码量相对多一些,不如全局方案优雅。
内容的提问来源于stack exchange,提问作者Hani Ghazi

