mongooseでスキーマにないフィールドを指定したらどうなる?知らんのか
スキーマレスが特徴のMongoDBを、ODMのmongoose経由で使う機会がありました。
「mongooseを使う=スキーマを定義する」ってことですが、存在しないフィールドの扱いがどうなるのか気になったので検証しました。
検証環境
バージョンは次のとおり。
- mongoose v9.0.1
- mongodb-memory-server(インメモリで検証)
検証コード
↓コード詳細
import mongoose from "mongoose";import { MongoMemoryServer } from "mongodb-memory-server";
// スキーマconst UserSchema = new mongoose.Schema( { name: String, }, // { strict: true },);
type UserModel = mongoose.Model<mongoose.InferSchemaType<typeof UserSchema>>;
// create検証async function testCreate(User: UserModel) { console.log("--- create 検証 ---"); await User.create({ name: "eetann", age: 99, // スキーマにないフィールド }); console.log("After create:", await User.findOne({ name: "eetann" }));}
// insertMany検証async function testInsertMany(User: UserModel) { console.log("--- insertMany 検証 ---"); await User.insertMany([ { name: "Taro", hobby: "reading" }, // スキーマにないフィールド { name: "Ichiro", score: 100 }, // スキーマにないフィールド ]); console.log("After insertMany:", await User.find({}));}
// updateOne 検証async function testUpdateOne(User: UserModel) { console.log("--- updateOne 検証 ---"); await User.updateOne({ name: "eetann" }, { $set: { country: "Japan" } }); console.log("After updateOne:", await User.findOne({ name: "eetann" }));}
// findOneAndUpdate 検証async function testFindOneAndUpdate(User: UserModel) { console.log("--- findOneAndUpdate 検証 ---"); const updated = await User.findOneAndUpdate( { name: "Taro" }, { $set: { level: 5 } }, { new: true }, ); console.log("After findOneAndUpdate:", updated);}
// new + saveの検証async function testNewAndSave(User: UserModel) { console.log("--- new + save 検証 ---"); const newUser = new User({ name: "Hanako", status: "active" }); // スキーマにないフィールド await newUser.save(); console.log("After new + save:", newUser);}
// 既存ドキュメントにスキーマにないフィールドを代入して saveasync function testModifyAndSave(User: UserModel) { console.log("--- modify + save 検証 ---"); const existingUser = await User.findOne({ name: "Ichiro" }); if (existingUser) { // 型エラーになる existingUser.role = "admin"; // スキーマにないフィールド await existingUser.save(); console.log("After modify + save:", existingUser); }}
// toObject / toJSON 検証async function testToObjectAndToJSON(User: UserModel) { console.log("--- toObject() / toJSON() 検証 ---"); const userForConvert = await User.findOne({ name: "eetann" }); if (userForConvert) { userForConvert.name = "eetann_updated"; // 存在するフィールドに代入 // 型エラーになる userForConvert.editor = "neovim"; // 存在しないフィールドに代入 // save無し
console.log("直接代入後 toObject():", userForConvert.toObject()); console.log("直接代入後 toJSON():", userForConvert.toJSON()); console.log("直接代入後 JSON.stringify():", JSON.stringify(userForConvert)); }}
// main関数async function main() { console.log(`Mongoose version: ${mongoose.version}`);
// In-memory MongoDB 起動 const mem = await MongoMemoryServer.create(); const uri = mem.getUri(); console.log("MongoDB URI:", uri);
// 接続 await mongoose.connect(uri);
const User = mongoose.model("User", UserSchema);
// 各検証を実行 await testCreate(User); await testInsertMany(User); await testUpdateOne(User); await testFindOneAndUpdate(User); await testNewAndSave(User); await testModifyAndSave(User); await testToObjectAndToJSON(User);
// 結果確認 console.log("--- Final result ---"); const data = await User.find({}); console.log("Final result:", data);
// 終了処理 await mongoose.disconnect(); await mem.stop();}
main().catch((err) => console.error(err));筆者はBunを使って実行しました。
bun run no-exist-field.ts↓実行結果
Mongoose version: 9.0.1MongoDB URI: mongodb://127.0.0.1:50273/--- create 検証 ---After create: { _id: new ObjectId('693d5635d9d42be0d197ba7e'), name: "eetann", __v: 0,}--- insertMany 検証 ---After insertMany: [ { _id: new ObjectId('693d5635d9d42be0d197ba7e'), name: "eetann", __v: 0, }, { _id: new ObjectId('693d5635d9d42be0d197ba81'), name: "Taro", __v: 0, }, { _id: new ObjectId('693d5635d9d42be0d197ba82'), name: "Ichiro", __v: 0, }]--- updateOne 検証 ---After updateOne: { _id: new ObjectId('693d5635d9d42be0d197ba7e'), name: "eetann", __v: 0,}--- findOneAndUpdate 検証 ---After findOneAndUpdate: { _id: new ObjectId('693d5635d9d42be0d197ba81'), name: "Taro", __v: 0,}--- new + save 検証 ---After new + save: { name: "Hanako", _id: new ObjectId('693d5635d9d42be0d197ba87'), __v: 0,}--- modify + save 検証 ---After modify + save: { _id: new ObjectId('693d5635d9d42be0d197ba82'), name: "Ichiro", __v: 0,}--- toObject() / toJSON() 検証 ---直接代入後 toObject(): { _id: new ObjectId('693d5635d9d42be0d197ba7e'), name: "eetann_updated", __v: 0,}直接代入後 toJSON(): { _id: new ObjectId('693d5635d9d42be0d197ba7e'), name: "eetann_updated", __v: 0,}直接代入後 JSON.stringify(): {"_id":"693d5635d9d42be0d197ba7e","name":"eetann_updated","__v":0}--- Final result ---Final result: [ { _id: new ObjectId('693d5635d9d42be0d197ba7e'), name: "eetann", __v: 0, }, { _id: new ObjectId('693d5635d9d42be0d197ba81'), name: "Taro", __v: 0, }, { _id: new ObjectId('693d5635d9d42be0d197ba82'), name: "Ichiro", __v: 0, }, { _id: new ObjectId('693d5635d9d42be0d197ba87'), name: "Hanako", __v: 0, }]検証結果
一言で書くと スキーマにないフィールドは無視されます 。これはmongooseのデフォルト設定がstrict: trueになっているからです。
具体的に見ていきましょう。
作成・更新系のメソッド
create、insertMany、updateOne、findOneAndUpdateは普通に無視されます。
await User.create({ name: "eetann", age: 99, // スキーマにないフィールド});new + save
newしてからsaveするやつでも同様に、スキーマの無いやつは無視されます。これも想定どおりだと思います。
const newUser = new User({ name: "Hanako", status: "active" // スキーマにないフィールド }); await newUser.save();途中から代入するパターン
〇〇.未定義フィールド = 値のように、途中で代入するパターンも試しました。同様に無視されます。
const user = await User.findOne({ name: "Ichiro" });user.role = "admin"; // スキーマにないフィールドawait user.save();TypeScriptを使ってれば型エラーになるので問題ないですが、 JavaScriptを使っていると気づけません 。
toObject・toJSON
toObject()・toJSON()は、存在するフィールドなら更新が反映され、存在しないなら無視されます。
const user = await User.findOne({ name: "eetann" });user.name = "eetann_updated"; // 存在するフィールドに代入user.role = "admin"; // スキーマにないフィールド
const hoge = { ...user.toObject(), // nameは更新・roleは含まれない hoge: 1,}
console.log(user.toJSON()) // nameは更新・roleは含まれないもちろんsaveしなければ、存在するフィールドの更新もDBには反映されません。
strictオプションで制御
公式ドキュメントに載っているとおりstrictオプションで制御できます。
まぁわざわざmongooseを使っている時点で「strict: falseにする人」はよほどの事情を抱えている少数派だと思います。
まとめ
デフォルトだと無視するけどJavaScriptだと分かりづらいので気を付けましょう。
以上、Mongooseの未定義フィールドの挙動でした。