🌽

mongooseでデフォルト値を指定した場合、値が存在しなくてもデフォルト値が返ってくるから気をつけろ

2025/01/24に公開

はじめに

業務でMongoDBを使い始めてしばらく経過した。ある程度使い慣れているPostgresSQLをはじめとしたRDB(Relational Data Base)とMongoDBの仕様の差分は大きく、RDBの仕様に関するイメージがMongoDBを利用するうえで落とし穴となることは多い。

今回落とし穴にはまりかけたのはdefault値の扱いだった。MongoDBそれ自体にはスキーマは存在しないが、OOMであるMongooseにはスキーマの概念がある。そしてdefault値も設定できる。

https://mongoosejs.com/docs/defaults.html

RDBのノリでdefault値を取り扱おうとしたら、全然取り扱いが違った。おかげで大事故になりかけたので、この場にメモしておきたい。

事象の概要

  1. まずはmongooseを利用してSchemaを定義し、default値を指定する
  2. 次にdefault値を設定してあるフィールドの値を削除する
  3. Model.find()経由で削除したフィールドを持つドキュメントを取得すると、2で削除したフィールドはdefault値が入って返ってくる
  4. .lean()をつけてドキュメントを取得すると、2で削除したフィールドは削除されて返ってくる

以下のコードで再現できる。node index.jsで実行してみる。

index.js
const { Schema, model, connect } = require('mongoose');

  
const testSchema = new Schema({
  name: { type: String, required: true },
  status: { type: String, default: 'active' },
  count: { type: Number, default: 0 },
  createdAt: { type: Date, default: Date.now }
});

const Test = model('Test', testSchema);

async function runTests() {
  await connect('mongodb://localhost:27017/test');
  console.log('=== 初期作成テスト ===');
  const doc1 = await Test.create({ name: 'test1' });
  console.log('作成直後:', doc1.toObject());
  /*
	作成直後: {
	  name: 'test1',
	  status: 'active',
	  count: 0,
	  _id: new ObjectId('6792ec9eba57f7d78a4de4ba'),
	  createdAt: 2025-01-24T01:27:58.343Z,
	  __v: 0
	}
  */
  console.log('\n=== フィールド削除===');
  await Test.updateOne(
    { _id: doc1._id },
    { $unset: { status: 1, count: 1 } }
  );

  console.log('\n=== 通常取得テスト ===');
  const normalDoc = await Test.findById(doc1._id);
  console.log('通常取得:', normalDoc?.toObject());
  /*
	通常取得: {
	  status: 'active', // default値が入っている
	  count: 0,  // default値が入っている
	  _id: new ObjectId('6792ec9eba57f7d78a4de4ba'),
	  name: 'test1',
	  createdAt: 2025-01-24T01:27:58.343Z,
	  __v: 0
	}
  */
  console.log('\n=== Lean取得テスト ===');
  const leanDoc = await Test.findById(doc1._id).lean();
  console.log('Lean取得:', leanDoc);
  /*
	Lean取得: {
	  _id: new ObjectId('6792ec9eba57f7d78a4de4ba'), // 削除されたフィールドが消えている
	  name: 'test1',
	  createdAt: 2025-01-24T01:27:58.343Z,
	  __v: 0
	}
  */

}
runTests().catch(console.error);
  • .lean()をつけて取得した場合、つまり plain old JavaScript objects (POJO)の場合、該当フィールドは削除されている
  • .lean()つけずに取得した場合、つまりMongoose Documentの場合、該当フィールドはdefault値が入ってくる

https://mongoosejs.com/docs/tutorials/lean.html

lean()で取得した場合はフィールドがなく、lean()をつけないで取得した場合はフィールドがdefault値で埋め込まれるのなら、hydrationの段階でdefault値が入ってくる仕様と推測される。だが、実際のところどうなのかわからない。

package.json

package.json
{
  "name": "mongoose-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/node": "^22.10.9",
    "mongoose": "^8.9.5",
    "ts-node": "^10.9.2",
    "typescript": "^5.7.3"
  }
}

Discussion