Mongoose Getters 与 Setters 完全指南:数据转换、隐私脱敏与本地化实践
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
导读
Mongoose 的 getters(取值器)与 setters(赋值器)允许你在读取或写入文档属性时执行自定义逻辑:getter 可以把 MongoDB 中存储的原始数据转换成更友好的形式(如邮箱脱敏、本地化展示),setter 则可以在数据写入 MongoDB 之前完成规范化(如统一小写、去除空格)。本文基于 docs/tutorials/getters-setters.md 展开,结合仓库源码(lib/schemaType.js、lib/document.js、lib/helpers/query/castUpdate.js)与测试用例(test/docs/getters-setters.test.js),系统讲解 getter/setter 的定义方式、执行时机、toJSON/toObject 联动、更新操作中的行为,以及利用$locals传递参数的进阶技巧,读完即可在生产代码中直接落地。
Getters:读取时的数据转换
基础用法:邮箱脱敏
假设有一个User集合,为了隐私保护,你希望在读取用户邮箱时对其脱敏。在 schema 中为路径声明get函数即可:
const userSchema = new Schema({ email: { type: String, get: obfuscate } }); // Mongoose 会把 MongoDB 中 `email` 的原始值传给 getter function obfuscate(email) { const separatorIndex = email.indexOf('@'); if (separatorIndex < 3) { // 'ab@gmail.com' -> '**@gmail.com' return email.slice(0, separatorIndex).replace(/./g, '*') + email.slice(separatorIndex); } // 'test42@gmail.com' -> 'te****@gmail.com' return email.slice(0, 2) + email.slice(2, separatorIndex).replace(/./g, '*') + email.slice(separatorIndex); } const User = mongoose.model('User', userSchema); const user = new User({ email: 'ab@gmail.com' }); user.email; // **@gmail.com对应测试见 test/docs/getters-setters.test.js,其中还验证了user.toJSON().email返回的是原始值'ab@gmail.com'——这正是下一节要强调的关键特性。
关键特性:getter 不改变底层存储
务必记住:getter 只影响读取时的返回值,不影响 MongoDB 中存储的原始数据。即使你保存了user,数据库中email字段依然是'ab@gmail.com'。getter 是"视图层"转换,不是"存储层"转换。
底层实现:getters 链式调用
从源码看,每个 SchemaType 维护了一个getters数组,SchemaType.prototype.get(fn)会把函数 push 进数组(lib/schemaType.js),真正执行时按顺序逐个调用:
// lib/schemaType.js 中 applyGetters 的核心逻辑 SchemaType.prototype.applyGetters = function(value, scope) { let v = value; const getters = this.getters; const len = getters.length; if (len === 0) { return v; } for (let i = 0; i < len; ++i) { v = getters[i].call(scope, v, this); } return v; };因此你可以在同一路径上注册多个 getter(多次调用.get(fn)),它们会按注册顺序依次对值进行变换;getter 内this指向当前文档对象。在文档读取路径上,lib/document.js 中的Document.prototype.get会在options.getters !== false时调用schema.applyGetters(obj, this),这也解释了为什么可以通过传{ getters: false }跳过 getter。
toJSON 与 Express res.json:getter 默认不生效
默认情况下,Mongoose不会在把文档转换成 JSON 时执行 getter,包括 Express 的res.json():
app.get(function(req, res) { return User.findOne(). // 这里的 email getter 不会执行 then(doc => res.json(doc)). catch(err => res.status(500).json({ message: err.message })); });原因在于toObject()/toJSON()的默认选项中getters为false(schema 选项toJSON与toObject默认无值,见 lib/schema.js,getter 仅在显式配置时启用)。
方案一:schema 级开启
const userSchema = new Schema({ email: { type: String, get: obfuscate } }, { toJSON: { getters: true } });方案二:全局开启
mongoose.set('toJSON', { getters: true });方案三:单次调用开启
app.get(function(req, res) { return User.findOne(). // 这里 email getter 会执行 then(doc => res.json(doc.toJSON({ getters: true }))). catch(err => res.status(500).json({ message: err.message })); });同理,toObject选项也支持getters: true,适用于console.log(doc.toObject())等场景;在代码中直接访问user.email则始终会触发 getter,与 JSON 序列化无关。
单次跳过 getter
如果需要取原始值,使用user.get()并传入{ getters: false }:
user.get('email', null, { getters: false }); // 'ab@gmail.com'这与源码中Document.prototype.get对options.getters !== false的判断一致(lib/document.js)。
Setters:写入前的数据规范化
基础用法:邮箱统一小写
假设你希望数据库中所有邮箱都小写存储,方便不区分大小写的检索。为路径声明set即可:
const userSchema = new Schema({ email: { type: String, set: v => v.toLowerCase() } }); const User = mongoose.model('User', userSchema); const user = new User({ email: 'TEST@gmail.com' }); user.email; // 'test@gmail.com' // email 的原始值已被小写化 user.get('email', null, { getters: false }); // 'test@gmail.com' user.set({ email: 'NEW@gmail.com' }); user.email; // 'new@gmail.com'注意与 getter 的区别:setter 是在写入前把值真正转换掉,所以连user.get('email', null, { getters: false })取到的也是小写后的结果,并且user.set(...)重新赋值时 setter 会再次触发。
底层实现:先 setter 后 cast
从源码看,setter 与 getter 一样被保存在setters数组中,SchemaType.prototype.set(fn)负责追加(lib/schemaType.js),执行流程是逆序逐个调用 setter,全部执行完后再做类型转换(cast):
// lib/schemaType.js 中 _applySetters / applySetters 的核心逻辑 SchemaType.prototype._applySetters = function(value, scope, init, priorVal, options) { let v = value; if (init) { return v; // 初始化阶段(如 hydrate)不执行 setter } const setters = this.setters; for (let i = setters.length - 1; i >= 0; i--) { v = setters[i].call(scope, v, priorVal, this, options); } return v; }; SchemaType.prototype.applySetters = function(value, scope, init, priorVal, options) { let v = this._applySetters(value, scope, init, priorVal, options); if (v == null) { return this._castNullish(v); } // 所有 setter 执行完之后才 cast(#665) v = this.cast(v, scope, init, priorVal, options); return v; };两个值得记住的细节:多个 setter 按逆序执行(后注册的先执行),以及只有init为 false 时才执行 setter——这意味着从 MongoDB 加载(hydration)数据时不会重复套用 setter,避免"已经小写过的值再小写"之类的幂等性隐患。
更新操作中的 setter
Mongoose 也会在updateOne()、updateMany()等更新操作中执行 setter。下面的例子中,updateOne()会以小写后的email进行 upsert:
await User.updateOne({}, { email: 'TEST@gmail.com' }, { upsert: true }); const doc = await User.findOne(); doc.email; // 'test@gmail.com'源码佐证:更新路径的 cast 过程会调用schema.applySetters(val, context)(lib/helpers/query/castUpdate.js),因此 setter 天然覆盖"文档赋值"与"更新操作"两条写入路径。
判断 setter 的执行上下文:this 指向
在 setter 函数中,this可能是正在被赋值的文档,也可能是正在执行的查询。如果不希望 setter 在updateOne()时执行(例如只想在文档实例上小写化),可以显式判断this是否是 Mongoose 文档:
const userSchema = new Schema({ email: { type: String, set: toLower } }); function toLower(email) { // 使用 updateOne() 或 updateMany() 时不转换 email if (!(this instanceof mongoose.Document)) { return email; } return email.toLowerCase(); } const User = mongoose.model('User', userSchema); await User.updateOne({}, { email: 'TEST@gmail.com' }, { upsert: true }); const doc = await User.findOne(); doc.email; // 'TEST@gmail.com' —— 更新路径上 setter 被跳过对应测试见 test/docs/getters-setters.test.js。
使用 $locals 为 getter/setter 传递参数
getter 和 setter 是普通函数,无法像调用普通函数那样直接传参。Mongoose 提供了文档属性$locals作为承载"程序自定义数据"的推荐位置——它不会与 schema 定义的字段冲突,而 getter/setter 内部this指向当前文档,因此可以在$locals上设置属性,再在 getter 中读取。
经典场景:国际化(i18n)。用一个子 schema 保存多种语言的文案,getter 根据$locals.language动态返回对应语言:
const internationalizedStringSchema = new Schema({ en: String, es: String }); const ingredientSchema = new Schema({ // 不把 name 设为普通字符串,而是语言代码到字符串的映射 name: { type: internationalizedStringSchema, // 访问 name 时,读取文档的 locale get: function(value) { return value[this.$locals.language || 'en']; } } }); const recipeSchema = new Schema({ ingredients: [{ type: mongoose.ObjectId, ref: 'Ingredient' }] }); const Ingredient = mongoose.model('Ingredient', ingredientSchema); const Recipe = mongoose.model('Recipe', recipeSchema); // 创建示例数据 const { _id } = await Ingredient.create({ name: { en: 'Eggs', es: 'Huevos' } }); await Recipe.create({ ingredients: [_id] }); // 通过 populate 的 transform 回调设置 $locals.language 实现本地化 const language = 'es'; const recipes = await Recipe.find().populate({ path: 'ingredients', transform: function(doc) { doc.$locals.language = language; return doc; } }); // 取到西班牙语名称 name.es assert.equal(recipes[0].ingredients[0].name, 'Huevos'); // 'Huevos'该示例的完整测试见 test/docs/getters-setters.test.js。这里的transform是populate()在子文档返回前执行的钩子,借助它把请求级语言(如req.query.lang)注入$locals,再通过 getter 无侵入地完成多语言展示,是生产环境中非常实用的组合拳。
与 ES6 原生 getter/setter 的差异
Mongoose setter 与 ES6 原生 setter 有本质区别:Mongoose setter 允许你转换将要存储的值,而 ES6 的set语法只是赋值拦截器,return返回值会被忽略、不写入任何存储:
class User { // 这样不会把 email 转成小写!因为 `email` 只是个 setter, // 真正的 email 属性没有存储任何数据 set email(v) { // eslint-disable-next-line no-setter-return return v.toLowerCase(); } } const user = new User(); user.email = 'TEST@gmail.com'; user.email; // undefined要在 ES6 中实现同等效果,你必须额外维护一个内部属性(如_email)并在 setter 中赋值、再为email定义配套的 getter。而 Mongoose 的 setter 由底层applySetters机制接管(先执行 setter 链,再 cast 后真正写入_doc),因此无需定义内部_email属性,也无需为email编写配套 getter。对应测试见 test/docs/getters-setters.test.js。
小结与最佳实践
| 能力点 | 关键结论 | 参考位置 |
|---|---|---|
| getter 只影响读取 | 不修改 MongoDB 存储的原始值 | docs/tutorials/getters-setters.md |
| getter 默认不进 JSON | 需配置toJSON: { getters: true }或toObject: { getters: true } | lib/schema.js |
| 单次跳过 getter | doc.get(path, null, { getters: false }) | lib/document.js |
| setter 先于 cast 执行 | 多个 setter 逆序调用,init阶段跳过 | lib/schemaType.js |
| setter 覆盖更新操作 | updateOne()/updateMany()同样执行 setter | lib/helpers/query/castUpdate.js |
this区分上下文 | setter 中this可能是文档或查询 | test/docs/getters-setters.test.js |
| 传递参数 | 用$locals存程序自定义数据,getter/setter 内this.$locals读取 | test/docs/getters-setters.test.js |
实践中建议:把校验(如格式检查)交给 validation 体系,把展示层转换(脱敏、本地化、单位换算)交给 getter,把存储层规范化(小写、trim、时区统一)交给 setter;涉及更新操作时务必确认 setter 的this上下文是否符合预期,必要时用this instanceof mongoose.Document做分支。若需深入了解文档读取与序列化的完整链路,可继续阅读 documents、guide(toJSON/toObject 选项) 与 API 参考。
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考