Mongoose 中的 Getters/Setters

Mongoose 的 getters 和 setters 允許您在取得或設定 Mongoose 文件的屬性時執行自訂邏輯。Getters 讓您可以將 MongoDB 中的資料轉換為更方便使用者使用的格式,而 setters 讓您可以在使用者資料傳送到 MongoDB 之前先進行轉換。

Getters (取值器)

假設您有一個 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) {
    // '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

請記住,getters 不會影響儲存在 MongoDB 中的基礎資料。如果您儲存 user,則資料庫中的 email 屬性將為 'ab@gmail.com'。

預設情況下,Mongoose 在將文件轉換為 JSON 時,包括 Express 的 res.json() 函數不會執行 getters。

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 }));
});

若要在將文件轉換為 JSON 時執行 getters,請在您的結構描述中將 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 }));
});

若要一次性略過 getters,請使用 user.get(),並將 getters 選項設為 false,如下所示。

user.get('email', null, { getters: false }); // 'ab@gmail.com'

Setters (設定器)

假設您想要確保資料庫中的所有使用者電子郵件都轉換為小寫,以便在搜尋時不必擔心大小寫。以下是一個範例 userSchema,它確保電子郵件為小寫。

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'

// The raw value of `email` is lowercased
user.get('email', null, { getters: false }); // 'test@gmail.com'

user.set({ email: 'NEW@gmail.com' });
user.email; // 'new@gmail.com'

Mongoose 也會在更新操作(例如 updateOne())中執行 setters。在以下範例中,Mongoose 將會 upsert (插入或更新) 一個文件,其中 email 為小寫。

await User.updateOne({}, { email: 'TEST@gmail.com' }, { upsert: true });

const doc = await User.findOne();
doc.email; // 'test@gmail.com'

在 setter 函數中,this 可以是要設定的文件或正在執行的查詢。如果您不希望在呼叫 updateOne() 時執行 setter,您可以新增一個 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: 'TEST@gmail.com' }, { upsert: true });

const doc = await User.findOne();
doc.email; // 'TEST@gmail.com'

使用 $locals 傳遞參數

您無法像對一般函數呼叫一樣將參數傳遞給 getter 和 setter 函數。若要設定或將其他屬性傳遞給您的 getters 和 setters,您可以使用文件的 $locals 屬性。

$locals 屬性是儲存任何程式定義的資料在您的文件上的首選位置,而不會與結構描述定義的屬性衝突。在您的 getter 和 setter 函數中,this 是正在存取的文件,因此您可以在 $locals 上設定屬性,然後在您的 getters 範例中存取這些屬性。例如,以下顯示如何使用 $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 Getters/Setters 的差異

Mongoose setters 與 ES6 setters 不同,因為它們允許您轉換正在設定的值。使用 ES6 setters,您需要儲存內部 _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 = 'TEST@gmail.com';

user.email; // undefined