問題点:ネストされたパスにおける型の不一致日常的な更新作業が失敗したため、この記事に辿り着いたのではないでしょうか。このエラーは、ドット記法(dot notation)を使用してサブフィールドを更新しようとした際、親フィールドがオブジェクトではない場合に発生します。MongoDBはドキュメントを「掘り下げよう」としますが、行き止まり(通常は文字列や数値)に突き当たってしまいます。
一般的なシナリオを見てみましょう。古いバージョンのアプリで作成された以下のようなユーザードキュメントがあるとします。
{
"_id": ObjectId("60a1b2c3d4e5f6g7h8i9j0k1"),
"name": "John Doe",
"address": "123 Main St"
}
その後、要件が変更されました。単一の文字列ではなく、詳細な住所データを保存することにしました。そこで、以下の更新を実行します。
db.users.updateOne(
{ "name": "John Doe" },
{ "$set": { "address.city": "ニューヨーク" } }
)
MongoDBは即座に失敗します。addressは現在文字列であるため、その中にcityを見つけることができません。プリミティブ値(基本型)をトラバース(走査)することはできないのです。
なぜこれが発生するのかデータベースは非常に厳密です。address.cityに到達するには、addressキーがコンテナ(オブジェクト)である必要があります。もし「123 Main St」のような文字列であれば、MongoDBは新しいデータをどこに配置すればよいか判断できません。この競合は通常、以下の3つの問題から発生します。
- スキーマの進化: データモデルをアップグレードしたが、古いレコードがレガシーな形式のまま残っている。- データの汚染: 古いスクリプトのバグにより、オブジェクトが期待される場所に文字列が挿入された。- パスの混同: 自分が想定している形とは異なる構造のネストされたパスをターゲットにしている。## 単一ドキュメントの手動修正修正が必要なレコードが1つか2つの場合は、親フィールド全体を置き換えます。サブフィールドを個別に更新しようとせず、文字列を新しいオブジェクトで上書きしてください。
db.users.updateOne(
{ "name": "John Doe" },
{ "$set": { "address": { "city": "ニューヨーク", "street": "123 Main St" } } }
)
これにより、トラバースのロジックを完全に回避できます。単に文字列を構造化されたオブジェクトに入れ替えるだけです。
大規模なコレクションの一括移行5万件のレガシーレコードがある場合、手動で修正するのは不可能です。コレクションを標準化するための移行スクリプトが必要です。これにより、ネストされた更新を実行する前に、すべてのドキュメントが新しいスキーマに従っていることを保証します。
1. 該当するドキュメントを特定するまず、古い文字列形式を使い続けているドキュメントがいくつあるか正確に把握します。
db.users.countDocuments({ "address": { "$type": "string" } })
2. 文字列をオブジェクトに変換する (MongoDB 4.2以降)最新のMongoDBバージョンでは、更新コマンド内でアグリゲーションパイプラインを使用できます。これは、フィールド型を変更しながら既存の値を参照できるため、非常に強力です。
db.users.updateMany(
{ "address": { "$type": "string" } },
[
{
"$set": {
"address": {
"fullAddress": "$address",
"city": "不明",
"migrated": true
}
}
}
]
)
この例では、古い文字列を新しい fullAddress フィールドに移動しています。また、将来のトラバースエラーを防ぐために city プレースホルダーを追加しています。
Node.js (Mongoose) での防御的コーディング本番環境のコードでは、データの不整合を想定しておく必要があります。移行が100%完了していない場合は、try-catchブロックを使用して型の不一致を適切に処理します。これにより、APIがユーザーに500エラーを返すのを防ぎます。
// より安全な更新パターン
try {
await User.updateOne(
{ _id: userId },
{ $set: { "address.city": "ニューヨーク" } }
);
} catch (err) {
if (err.code === 28 || err.message.includes('cannot use the part')) {
// フォールバック: フィールドがおそらく文字列であるため、上書きする。
await User.updateOne(
{ _id: userId },
{ $set: { address: { city: "ニューヨーク" } } }
);
} else {
throw err;
}
}
検証と防止修正を適用したら、すべてのドキュメントで「トラバース」パスが問題ないことを確認します。以下のクエリを実行してください。
db.users.find({ "address": { "$exists": true, "$not": { "$type": "object" } } })
この結果が0件であれば、コレクションはクリーンな状態です。その状態を維持するために、MongoDB Schema Validationの導入を検討してください。addressフィールドが常にobjectであることを強制するルールを設定し、将来的に文字列が挿入されるのを防ぐことができます。

