Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB nested Document Validation for sub-documents

Tags:

mongodb

I got a document structured like the following. My question is how do I do the nested part "roles" validation on the database side. My requirements are:

  • the roles size could be 0 or more than 1.
  • the presence of name and created_by for a role if a role is created.

    {
      "_id": "123456",
      "name": "User Name",
      "roles": [
        {
          "name": "mobiles_user",
           "last_usage_at": {
             "$date": 1457000592991
            },
            "created_by": "987654",
            "created_at": {
              "$date": 1457000592991
            }
        },
        {
          "name": "webs_user",
           "last_usage_at": {
             "$date": 1457000592991
            },
            "created_by": "987654",
            "created_at": {
              "$date": 1457000592991
            }
        },
      ]
    }
    

At the moment, I am only doing the following for those none nested attributes:

db.createCollection( "users",
   { "validator" : {
     "_id" : {
         "$type" : "string"
      },
      "email" : {
         "$regex" : /@gmail\.com$/
      },
      "name" : {
         "$type" : "string"
      }
   }
} )

Could anyone please advise that how to do the nested document validation?

like image 619
LPing Avatar asked Mar 08 '16 23:03

LPing


2 Answers

Yes, you can validate all sub-documents in a document by negating $elemMatch, and you can ensure that the size is not 1. It's sure not pretty though! And not exactly obvious either.

> db.createCollection('users', {
...   validator: {
...     name: {$type: 'string'},
...     roles: {$exists: 'true'},
...     $nor: [
...       {roles: {$size: 1}},
...       {roles: {$elemMatch: {
...         $or: [
...           {name: {$not: {$type: 'string'}}},
...           {created_by: {$not: {$type: 'string'}}},
...         ]
...       }}}
...     ],
...   }  
... })
{ "ok" : 1 }

This is confusing, but it works! What it means is only accept documents where neither the size of roles is 1 nor roles has an element with a name that isn't a string or a created_by that isn't a string.

This is based upon the fact that in logic terms,

for all x: f(x) and g(x)

Is equivalent to

not exists x s.t.: not f(x) or not g(x)

We have to use the latter since MongoDB only gives us an exists operator.

Proof

Valid documents work:

> db.users.insert({
...   name: 'hello',
...   roles: [],
... })
WriteResult({ "nInserted" : 1 })

> db.users.insert({
...   name: 'hello',
...   roles: [
...     {name: 'foo', created_by: '2222'},
...     {name: 'bar', created_by: '3333'},
...   ]
... })
WriteResult({ "nInserted" : 1 })

If a field is missing from roles, it fails:

> db.users.insert({
...   name: 'hello',
...   roles: [
...     {name: 'foo', created_by: '2222'},
...     {created_by: '3333'},
...   ]
... })
WriteResult({
    "nInserted" : 0,
    "writeError" : {
        "code" : 121,
        "errmsg" : "Document failed validation"
    }
})

If a field in roles has the wrong type, it fails:

> db.users.insert({
...   name: 'hello',
...   roles: [
...     {name: 'foo', created_by: '2222'},
...     {name: 'bar', created_by: 3333},
...   ]
... })
WriteResult({
    "nInserted" : 0,
    "writeError" : {
        "code" : 121,
        "errmsg" : "Document failed validation"
    }
})

If roles has size 1 it fails:

> db.users.insert({
...   name: 'hello',
...   roles: [
...     {name: 'foo', created_by: '2222'},
...   ]
... })
WriteResult({
    "nInserted" : 0,
    "writeError" : {
        "code" : 121,
        "errmsg" : "Document failed validation"
    }
})

The only thing I can't figure out unfortunately is how to ensure that roles is an array. roles: {$type: 'array'} seems to fail everything, I presume because it's actually checking that the elements are of type 'array'?

like image 177
Andy Avatar answered Nov 09 '22 05:11

Andy


Edit: this answer is not correct, it is possible to validate all sub-documents in the array. See answer: https://stackoverflow.com/a/43102783/200224

You can't really. You can do things like:

"roles.name": { "$type": "string" }

But all that really means is at "at least one" of those properties need match the specified type. That means this would actually be valid:

{
    "_id" : "123456",
    "name" : "User Name",
    "roles" : [
            {
                    "name" : "mobiles_user",
                    "last_usage_at" : ISODate("2016-03-03T10:23:12.991Z"),
                    "created_by" : "987654",
                    "created_at" : ISODate("2016-03-03T10:23:12.991Z")
            },
            {
                    "name" : "webs_user",
                    "last_usage_at" : ISODate("2016-03-03T10:23:12.991Z"),
                    "created_by" : "987654",
                    "created_at" : ISODate("2016-03-03T10:23:12.991Z")
            },
            {
                    "name" : 1
            }
    ]
}

It is afterall "documement validation" and that is by nature not well suited to sub-documents in arrays, or any data in a contained array really.

The core of the implementation relies on expressions available to query operators, and since MongoDB lacks anythin in standard query expressions that equates to "All array entries must match this value" without being directly specific then it's not possible to express as a validator condition.

The only posibility to check array content like that in a "query" expression is using $where, and that is noted to not be an available option with document validation.

Even the $size operator available for queries must match a specific "size" value, and cannot use an in-equality condition. So you "could" verify a strict size, but not a minimal size, unless:

"roles.0": { "$exists": true }

This is a feature in "infancy" and somewhat experimental, so there is the possibility that future releases may address this.

But for now, your better option is to do such "schema validation" in client side code ( where you will get a lot better exception reporting ) instead. There are many libraries already existing that take that approach.

like image 6
Blakes Seven Avatar answered Nov 09 '22 06:11

Blakes Seven