Mongoose のゲッター/セッター

Mongoose のゲッターとセッターを使用すると、Mongoose ドキュメントのプロパティを取得または設定するときにカスタムロジックを実行できます。ゲッターを使用すると MongoDB のデータをより使いやすい形式に変換でき、セッターを使用すると、ユーザーデータが MongoDB に到達する前に変換できます。

ゲッター

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

ゲッターは MongoDB に格納された基本的なデータには影響しないことに注意してください。user を保存すると、データベース内の email プロパティは 'ab@gmail.com' になります。

既定では、Mongoose はドキュメントを JSON に変換するときに 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 }));
});

ドキュメントを 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 }));
});

一時的にゲッターをスキップするには、getters オプションを false に設定した user.get() を使用します(以下を参照)。

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

セッター

データベース内のすべてのユーザーメールを小文字にすることで、大文字小文字を心配することなく簡単に検索できるようにしたいとします。以下はメールが小文字になるよう保証する 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() のような更新操作でもセッターを実行します。Mongoose は、次の例のように、小文字の email を使用してドキュメントを 更新します

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

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

セッター関数では、this は設定されているドキュメントまたは実行されているクエリーのいずれかになります。updateOne() を呼び出したときにセッターを実行したくない場合は、this が Mongoose ドキュメントであるかどうかをチェックする if ステートメントを追加します(以下を参照)。

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 を使用してパラメーターを渡す

普通の関数呼び出しのようにゲッター関数とセッター関数にパラメーターを渡すことはできません。ゲッターとセッターに構成または追加のプロパティを渡すには、ドキュメントの $locals プロパティを使用できます。

$locals プロパティは、プログラムで定義されたデータをスキーマで定義されたプロパティと競合せずにドキュメントに格納するために推奨される場所です。ゲッター関数とセッター関数では、this はアクセスするドキュメントのため、$locals にプロパティを設定し、ゲッターの例でそれらのプロパティにアクセスします。たとえば、$locals を使用して、さまざまな言語で文字列を返すカスタムゲッターの言語を構成する方法を以下に示します。

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 ゲッター/セッターとの違い

Mongoose のセッターは ES6 セッター とは異なります。ES6 セッターでは、セットされる値を変換することができます。ES6 セッターでは、セッターを使用するために内部 _email プロパティを格納する必要があります。Mongoose では、内部 _email プロパティを定義する必要はありません。また、email に対応するゲッターを定義する必要もありません。

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