Mongoose 中的 Getter/Setter

Mongoose 的 getter 和 setter 允许您在获取或设置 Mongoose 文档 上的属性时执行自定义逻辑。Getter 允许您将 MongoDB 中的数据转换为更友好的形式,而 Setter 允许您在用户数据进入 MongoDB 之前对其进行转换。

Getter

假设您有一个 User 集合,并且您希望对用户电子邮件进行混淆以保护用户的隐私。以下是一个基本的 userSchema,它会对用户的电子邮件地址进行混淆。

const userSchema = new Schema({
  email: {
    type: String,
    get: obfuscate
  }
});

// Mongoose passes the raw value in MongoDB `email` to the getter
function obfuscate(email) {
  const separatorIndex = email.indexOf('@');
  if (separatorIndex < 3) {
    // '[email protected]' -> '**@gmail.com'
    return email.slice(0, separatorIndex).replace(/./g, '*') +
      email.slice(separatorIndex);
  }
  // '[email protected]' -> '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: '[email protected]' });
user.email; // **@gmail.com

请记住,getter 不会影响存储在 MongoDB 中的底层数据。如果您保存 user,则 email 属性在数据库中将是 '[email protected]'。

默认情况下,Mongoose 在将文档转换为 JSON 时不会执行 getter,包括 Express 的 res.json() 函数

app.get(function(req, res) {
  return User.findOne().
    // The `email` getter will NOT run here
    then(doc => res.json(doc)).
    catch(err => res.status(500).json({ message: err.message }));
});

要将 getter 应用于将文档转换为 JSON 时,请将 toJSON.getters 选项设置为您的模式中的 true,如下所示。

const userSchema = new Schema({
  email: {
    type: String,
    get: obfuscate
  }
}, { toJSON: { getters: true } });

// Or, globally
mongoose.set('toJSON', { getters: true });

// Or, on a one-off basis
app.get(function(req, res) {
  return User.findOne().
    // The `email` getter will run here
    then(doc => res.json(doc.toJSON({ getters: true }))).
    catch(err => res.status(500).json({ message: err.message }));
});

要在一个事件中跳过 getter,请使用 user.get() 并将 getters 选项设置为 false,如下所示。

user.get('email', null, { getters: false }); // '[email protected]'

Setter

假设您希望确保数据库中的所有用户电子邮件都是小写的,以便您可以轻松搜索而不用担心大小写。以下是一个示例 userSchema,它确保电子邮件是小写的。

const userSchema = new Schema({
  email: {
    type: String,
    set: v => v.toLowerCase()
  }
});

const User = mongoose.model('User', userSchema);

const user = new User({ email: '[email protected]' });
user.email; // '[email protected]'

// The raw value of `email` is lowercased
user.get('email', null, { getters: false }); // '[email protected]'

user.set({ email: '[email protected]' });
user.email; // '[email protected]'

Mongoose 还将在更新操作(如 updateOne())中运行 setter。在下面的示例中,Mongoose 将 使用小写 email 插入文档

await User.updateOne({}, { email: '[email protected]' }, { upsert: true });

const doc = await User.findOne();
doc.email; // '[email protected]'

在 setter 函数中,this 可以是正在设置的文档或正在运行的查询。如果您不希望 setter 在您调用 updateOne() 时运行,您可以添加一个 if 语句来检查 this 是否是 Mongoose 文档,如下所示。

const userSchema = new Schema({
  email: {
    type: String,
    set: toLower
  }
});

function toLower(email) {
  // Don't transform `email` if using `updateOne()` or `updateMany()`
  if (!(this instanceof mongoose.Document)) {
    return email;
  }
  return email.toLowerCase();
}

const User = mongoose.model('User', userSchema);
await User.updateOne({}, { email: '[email protected]' }, { upsert: true });

const doc = await User.findOne();
doc.email; // '[email protected]'

使用 $locals 传递参数

您不能像对普通函数调用那样将参数传递给 getter 和 setter 函数。要配置或将附加属性传递给 getter 和 setter,您可以使用文档的 $locals 属性。

$locals 属性是存储您在文档上定义的任何程序定义数据(而不与模式定义的属性冲突)的首选位置。在 getter 和 setter 函数中,this 是正在访问的文档,因此您在 $locals 上设置属性,然后在 getter 示例中访问这些属性。例如,以下显示了如何使用 $locals 来配置自定义 getter 的语言,该 getter 以不同的语言返回字符串。

const internationalizedStringSchema = new Schema({
  en: String,
  es: String
});

const ingredientSchema = new Schema({
  // Instead of setting `name` to just a string, set `name` to a map
  // of language codes to strings.
  name: {
    type: internationalizedStringSchema,
    // When you access `name`, pull the document's 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);

// Create some sample data
const { _id } = await Ingredient.create({
  name: {
    en: 'Eggs',
    es: 'Huevos'
  }
});
await Recipe.create({ ingredients: [_id] });

// Populate with setting `$locals.language` for internationalization
const language = 'es';
const recipes = await Recipe.find().populate({
  path: 'ingredients',
  transform: function(doc) {
    doc.$locals.language = language;
    return doc;
  }
});

// Gets the ingredient's name in Spanish `name.es`
assert.equal(recipes[0].ingredients[0].name, 'Huevos'); // 'Huevos'

与 ES6 Getter/Setter 的区别

Mongoose setter 不同于 ES6 setter,因为它们允许您转换正在设置的值。使用 ES6 setter,您需要存储一个内部 _email 属性来使用 setter。使用 Mongoose,您不需要定义内部 _email 属性或为 email 定义相应的 getter。

class User {
  // This won't convert the email to lowercase! That's because `email`
  // is just a setter, the actual `email` property doesn't store any data.
  // also eslint will warn about using "return" on a setter
  set email(v) {
    // eslint-disable-next-line no-setter-return
    return v.toLowerCase();
  }
}

const user = new User();
user.email = '[email protected]';

user.email; // undefined