4

Let's say I have a collection of documents that look like this:

{
    "_id" : ObjectId("5afa6df3a24cdb1652632ef5"),
    "createdBy" : {
        "_id" : "59232a1a41aa651ddff0939f"
    },
    "owner" : {
        "_id" : "5abc4dc0f47f732c96d84aac"
    },
    "acl" : [
        {
            "profile" : {
                "_id" : "59232a1a41aa651ddff0939f"
            }
        },
        {
            "profile" : {
                "_id" : "5abc4dc0f47f732c96d84aac"
            }
        }
    ]
}

I want to find all documents where createdBy._id != owner._id, AND where the createdBy._id appears in one of the entries in the acl array. Eventually, I will want to update all such documents to set the owner._id field to equal the createdBy._id field. For now, I'm just trying to figure out how to query the subset of documents I want to update.

So far, I have come up with this:

db.boards.find({
  $where: "this.createdBy._id != this.owner._id", 
  $where: function() {
    return this.acl.some(
      function(e) => {
        e.profile._id === this.createdBy._id
      }, this);
  }
)

(I have used ES5 syntax just in case ES6 isn't ok)

But when I run this query, I get the following error:

Error: error: { "ok" : 0, "errmsg" : "TypeError: e.profile is undefined :\n_funcs2/<@:2:36\n_funcs2@:2:12\n", "code" : 139 }

How do I perform this query / what is going on here? I would have expected my query to work, based on the docs I've read. Above, e should be an element of the acl array, so I expect it to have a field profile, but that doesn't seem to be the case.

Note, I'm using Mongo 3.2, so I can't use $expr, which I've seen some resources suggest is a possibility.

Resolution

It turns out that I had made an incorrect assumption about the schema of this collection. The reason I was running into the above error is because some documents have an acl array with an element that doesn't have a profile field. The below query checks for this case. It also has a single $where, because the way I had written it originally (with two) seemed to end up giving me an OR of the conditions instead of an AND.

db.boards.find({
  $where: function() {
    return this.acl.some(
      function(e) => {
        e.profile !== undefined && e.profile._id === this.createdBy._id && this.createdBy._id != this.owner._id
      }, this);
  }
)
Kevin
  • 95
  • 9
  • Is there something in the provided answer that you believe does not address your question? If so then please comment on the answer to clarify what exactly needs to be addressed that has not. If it does in fact answer the question you asked then please note to [Accept your Answers](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work) to the questions you ask – Neil Lunn May 29 '18 at 06:44

1 Answers1

3

You can still use aggregate() here with MongoDB 3.2, but just using $redact instead:

db.boards.aggregate([
  { "$redact": {
    "$cond": {
      "if": {
        "$and": [
          { "$ne": [ "$createdBy._id", "$owner._id" ] },
          { "$setIsSubset": [["$createdBy._id"], "$acl.profile._id"] }
        ]
      },
      "then": "$$KEEP",
      "else": "$$PRUNE"
    }
  }}
])

Or with $where for the MongoDB 3.2 shell, you just need to keep a scoped copy of this, and your syntax was a bit off:

db.boards.find({
  "$where": function() {
    var self = this;
    return (this.createdBy._id != this.owner._id)
      && this.acl.some(function(e) {
        return e.profile._id === self.createdBy._id
     })
  }
})

Or in an ES6 compatible environment then:

db.boards.find({
  "$where": function() {
    return (this.createdBy._id != this.owner._id)
      && this.acl.some(e => e.profile._id === this.createdBy._id)
  }
})

The aggregate is the most performant option of the two and should always be preferable to using JavaScript evalulation

And for what it's worth, the newer syntax with $expr would be:

db.boards.find({
  "$expr": {
    "$and": [
      { "$ne": [ "$createdBy._id", "$owner._id" ] },
      { "$in": [ "$createdBy._id", "$acl.profile._id"] }
    ]
  }
})

Using $in in preference to $setIsSubset where the syntax is a little shorter.


NOTE The only reason the JavaScript comparison here works is because you have mistakenly stored ObjectId values as "strings" in those fields. Where there is a "real" ObjectId just like in the _id field, the comparison needs to take the "string" from valueOf() in order to compare:

    return (this.createdBy._id.valueOf() != this.owner._id.valueOf())
      && this.acl.some(e => e.profile._id.valueOf() === this.createdBy._id.valueOf())

Without that it's actually an "Object Comparison" with JavaScript and { a: 1 } === { a: 1 } is actually false. So avoiding that complexity is another reason there are native operators for this instead.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • @Ashish They often do as I just type them in here mostly and this was done over breakfast. Corrected naming and missing braces. – Neil Lunn May 26 '18 at 07:11
  • Hi Neil, thanks for the response, though I need a little more help. Regarding the first query (using `aggregate()`), I see it returning results where the `owner._id` = `createdBy._id`, which I don't want. I'm also going to want to update all resulting documents to set the `owner._id` to equal the `createdBy._id` (I'll edit my question to include this). I'm not sure if I can do `updateMany()` using an aggregation. Regarding the next two (javascript) queries, I still get the same error message about "e.profile is undefined" – Kevin May 29 '18 at 18:20
  • @Kevin None of the examples here return where those two fields match because they all have an explicit condition that says they should not. If you are seeing a different result then you probably have a "string" in one field and an `ObjectId` in another. Solving that is a different question. You can update using either `$expr` where available or `$where`, and I have run the queries by copying the data you presented in the question and my code and it all works fine. If you are doing something different then don't and I suggest you also copy the document from here and run the queries from here. – Neil Lunn May 29 '18 at 21:31
  • @Kevin Since there is in fact "no error" with the queries here and it works as designed with the actual data supplied in the question, then you have a means of confirming that and moving on, If you have a new question then [Ask a New Question](https://stackoverflow.com/questions/ask). But the question asked here has been adequately answered. You were in fact given a detailed "NOTE" on `ObjectId` as your mistake seems quite clear. – Neil Lunn May 29 '18 at 21:33
  • So it turns out, the reason that the TypeError was happening is because some documents have `acl` array elements that do not have a `profile` field. This was my oversight, as I had made an incorrect assumption about the schema of this collection. I'll add a note about this. Thanks. – Kevin May 30 '18 at 00:59