原生MongoDB findOne返回对象丢失类方法的原因与解决方案
MongoDB是文档型数据库,存储的是纯结构化BSON数据,仅会保留对象的自有可枚举属性,不会存储JavaScript类原型链上挂载的方法、继承关系这类语言层面的信息。
- 你调用
insertOne(this)写入数据时,MongoDB Node驱动会自动对传入的User实例做序列化,仅提取实例上的email、hashedPassword、dateCreated这类属性字段存入集合,定义在User原型上的test()、saveToDb()、signToken()等方法根本不会被写入数据库。 - 你调用
findOne()查询数据时,驱动返回的是根据库中存储的文档直接生成的普通JavaScript对象,该对象的原型指向默认的Object.prototype,并非User类的原型,自然访问不到User类上定义的任何方法。
以下方案均无需引入Mongoose,可直接在现有代码基础上修改:
方案1:查询后手动重构类实例(最稳妥)
拿到查询返回的普通文档后,调用User构造函数重新生成实例,再把数据库中存储的字段值逐一赋值到实例上,避免构造函数默认逻辑覆盖库中真实数据。
以findByEmail方法为例,修改后代码如下:static async findByEmail(email: string) { try { const collection = getCollection(USERS_COLLECTION_NAME); const doc = await collection.findOne({ email }); if (!doc) return; // 重新构造User实例 const user = new User(doc.email, doc.hashedPassword); // 覆盖构造函数自动生成的创建时间,使用数据库存储的真实值 user.dateCreated = doc.dateCreated; // 后续类新增字段可在此处补充赋值逻辑 console.log('user.test', user.test); // 可正常访问方法 return user; } catch (err) { throw err; } }该方案优点是逻辑完全可控,不会出现原型污染问题;缺点是类新增字段时需要同步修改映射逻辑,适合字段数量不多、对稳定性要求高的场景。
方案2:直接修改返回对象的原型(代码量最少)
不需要逐字段映射,直接通过Object.setPrototypeOf把查询返回的普通对象的原型指向User类的原型,即可让对象访问到User上定义的方法。
示例代码:static async findByEmail(email: string) { try { const collection = getCollection(USERS_COLLECTION_NAME); const doc = await collection.findOne({ email }); if (!doc) return; // 绑定原型到User类 Object.setPrototypeOf(doc, User.prototype); const user = doc as User; console.log('user.test', user.test); // 可正常访问方法 return user; } catch (err) { throw err; } }注意:该方案有轻微性能损耗,且不会触发User构造函数中的初始化、校验逻辑,需要确保数据库中存储的字段格式完全符合类的定义要求。如果是查询多条数据(比如
fetchAll方法),需要遍历结果集给每个文档对象单独绑定原型。方案3:封装统一转换方法(易维护)
在User类上新增统一的静态转换方法,所有查询返回的文档都走这个方法转成User实例,避免重复写转换逻辑,后续类字段变更只需要修改这一处代码即可。
示例代码:// 类上新增静态转换方法 static fromDoc(doc: Partial<User>): User | null { if (!doc) return null; const user = new User(doc.email, doc.hashedPassword); user.dateCreated = doc.dateCreated; // 此处可统一加字段格式校验、默认值补全等逻辑 return user; }所有查询场景拿到结果后,直接调用
User.fromDoc(doc)即可拿到合法的User实例,包括fetchAll这类批量查询的场景,只需要对结果数组做map转换即可。
内容的提问来源于stack exchange,提问作者user8758206

