🌽
mongooseでデフォルト値を指定した場合、値が存在しなくてもデフォルト値が返ってくるから気をつけろ
はじめに
業務でMongoDBを使い始めてしばらく経過した。ある程度使い慣れているPostgresSQLをはじめとしたRDB(Relational Data Base)とMongoDBの仕様の差分は大きく、RDBの仕様に関するイメージがMongoDBを利用するうえで落とし穴となることは多い。
今回落とし穴にはまりかけたのはdefault値の扱いだった。MongoDBそれ自体にはスキーマは存在しないが、OOMであるMongooseにはスキーマの概念がある。そしてdefault値も設定できる。
RDBのノリでdefault値を取り扱おうとしたら、全然取り扱いが違った。おかげで大事故になりかけたので、この場にメモしておきたい。
事象の概要
- まずはmongooseを利用してSchemaを定義し、default値を指定する
- 次にdefault値を設定してあるフィールドの値を削除する
-
Model.find()
経由で削除したフィールドを持つドキュメントを取得すると、2で削除したフィールドはdefault値が入って返ってくる -
.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値が入ってくる
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