MongoDBのDocument Validationを試してみた

2017.02.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、菊池です。

MongoDB の大きな特徴の1つに、スキーマレスで事前に構造を定義せずにデータを投入できるというものがあります。一方で、データ構造を規定できないために意図しないデータが入り込んでしまうというデメリットもあります。

MongoDB 3.2以降のバージョンではDocument Validationの機能により、保存するデータをコレクション毎にある程度制限することが可能になりました。

Document Validation

Document Validationでは、コレクション毎に挿入/更新するデータに対し検証することができます。コレクションに対してValidatorオプションを使用して定義します。

Document Validationの指定には、$ near、$ nearSphere、$ text、$ whereを除く、任意のクエリ演算子が利用できます。詳細は公式ドキュメントを参照してください。

やってみた

Document Validationはコレクション作成時にも指定が可能ですし、既存のコレクションに後から適用することも可能です。制限のモードにはデフォルトのstrictと、moderateがあり、moderateでは適用時にすでに存在するドキュメントには制限が適用されません。

validationLevel:strict(デフォルト)の動作

まずはコレクションにデータを挿入します。この時点では特に制限をしていません。

> db.contacts.insert({name:"hoge1", city: "Tokyo", phone:"090-xxxx-xxxx"});
WriteResult({ "nInserted" : 1 })
> db.contacts.insert({name:"hoge2", city: "Kanagawa", email:"hoge2@example1.com"});
WriteResult({ "nInserted" : 1 })
> db.contacts.insert({name:"hoge3", city: "Chiba" });
WriteResult({ "nInserted" : 1 })

このcontactsコレクションに対して、Document Validationによる制限を適用します。phoneまたはemailの項目を必須にします。

> db.runCommand( {
   collMod: "contacts",
   validator: { $or: [ { phone: { $exists: true } }, { email: { $exists: true } } ] }
} )
{ "ok" : 1 }

この状態で、必須項目を含まないデータをInsertしようとすると、以下のようにエラーとなります。

> db.contacts.insert({name:"hoge4", city: "Tokyo"});
WriteResult({
       	"nInserted" : 0,
       	"writeError" : {
       		"code" : 121,
       		"errmsg" : "Document failed validation"
       	}
})

必須項目を含むデータをInsertすると、問題なく書き込みがされます。

> db.contacts2.insert({name:"hoge4", city: "Tokyo", phone:"090-zzzz-zzzz"});
WriteResult({ "nInserted" : 1 })

また、すでに存在していたデータで、必須項目を含んでいないデータをUpdateしてみます。必須項目を含まない形で更新しようとすると、エラーになってしまいます。

> db.contacts.update({name:"hoge3"}, {name:"hoge3", city: "Saitama"})
WriteResult({
       	"nMatched" : 0,
       	"nUpserted" : 0,
       	"nModified" : 0,
       	"writeError" : {
       		"code" : 121,
       		"errmsg" : "Document failed validation"
       	}
})

必須のキーを設定してUpdateすると、問題なく更新が可能です。

> db.contacts.update({name:"hoge3"}, {name:"hoge3", city: "Saitama", email:"hoge3@example1.com"})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

validationLevel:moderateの動作

続いて、validationLevel: "moderate"を指定してDocument Validationを設定してみます。validationLevel: "moderate"とすることで、すでに存在しているデータに対しては制限が非適用となります。

まずはコレクションcontacts2にデータを投入します。

> db.contacts2.insert({name:"hoge1", city: "Tokyo", phone:"090-xxxx-xxxx"});
WriteResult({ "nInserted" : 1 })
> db.contacts2.insert({name:"hoge2", city: "Kanagawa", email:"hoge2@example1.com"});
WriteResult({ "nInserted" : 1 })
> db.contacts2.insert({name:"hoge3", city: "Chiba" });
WriteResult({ "nInserted" : 1 })

validationLevel: "moderate"を指定してDocument Validationを適用します。

> db.runCommand( {
   collMod: "contacts2",
   validator: { $or: [ { phone: { $exists: true } }, { email: { $exists: true } } ] },
   validationLevel: "moderate"
} )

この状態で、必須項目を含まないドキュメントを更新します。

> db.contacts2.update({name:"hoge3"}, {name:"hoge3", city: "Saitama"})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

必須項目を含まなくても更新が可能でした。

> db.contacts2.find()
{ "_id" : ObjectId("58b43a0f20974cf9fd1ad1b4"), "name" : "hoge1", "city" : "Tokyo", "phone" : "090-xxxx-xxxx" }
{ "_id" : ObjectId("58b43a0f20974cf9fd1ad1b5"), "name" : "hoge2", "city" : "Kanagawa", "email" : "hoge2@example1.com" }
{ "_id" : ObjectId("58b43a0f20974cf9fd1ad1b6"), "name" : "hoge3", "city" : "Saitama" }

ちゃんと更新されてますね。

一方で、新規にInsertするデータには制限が有効になります。

> db.contacts2.insert({name:"hoge5", city: "Tokyo"});
WriteResult({
       	"nInserted" : 0,
       	"writeError" : {
       		"code" : 121,
       		"errmsg" : "Document failed validation"
       	}
})

さいごに

いかがでしょうか。

MongoDBの大きな特徴として、事前に定義したデータ構造に縛られずにデータ投入が可能という点があります。一方で、その自由度の高さから不適切なデータが入り込んでしまうという欠点もありました。そのため、不適切なデータが入らないようにするためにはアプリケーション側でデータの検証を実装する必要がありました。

Document Validationを適切に設定することで、不正なデータが入らないようデータベース側で制限が可能になります。必須のデータ項目などに設定しておくことでデータの正常性が担保できますので、うまく利用して行きましょう。