在数组中查找一个字段与另一个字段进行比较的文档

4

假设我有一组长这样的文件:

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

我希望找到所有文档,其中createdBy._id!= owner._id,并且createdBy._id出现在acl数组的一个条目中。最终,我将希望更新所有这样的文档,以设置owner._id字段等于createdBy._id字段。目前,我只是试图弄清楚如何查询我想要更新的文档子集。
到目前为止,我想到了以下内容:
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);
  }
)

(为了保险起见,我使用了ES5语法)

但是当我运行这个查询时,我得到了以下错误:

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

我该如何执行这个查询 / 这里发生了什么?根据我阅读的文档,我本来期望我的查询能够正常工作。在上面的代码中,e应该是acl数组的一个元素,因此我希望它有一个profile字段,但事实并非如此。

请注意,我正在使用Mongo 3.2,因此我不能使用一些资源中建议的$expr

解决方法

事实证明,我对这个集合的模式做出了错误的假设。我遇到上述错误的原因是因为一些文档具有一个acl数组,其中一个元素没有profile字段。下面的查询检查了这种情况。它还有一个单一的$where,因为我最初编写它的方式(使用两个$where)似乎会给我一个条件的OR而不是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);
  }
)

提供的答案是否有什么让您认为无法解决您的问题?如果有,请在答案中发表评论,以澄清需要解决的具体问题。如果它确实回答了您提出的问题,请注意接受答案并关闭您的问题。 - Neil Lunn
1个回答

3

在 MongoDB 3.2 中,您仍然可以使用 aggregate(),但只需改用$redact即可:

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

如果使用 MongoDB 3.2 shell 中的 $where,你只需要保留一个有范围的 this 副本,并且你的语法有点问题:

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
     })
  }
})

如果在支持ES6的环境中,则:

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

在这两种选择中,“聚合”是性能最佳的选项,应该始终优先考虑使用它,而不是使用JavaScript评估。

值得注意的是,使用$expr的新语法为:

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

在语法稍短的情况下,使用$in而不是$setIsSubset


注意 这里 JavaScript 比较起作用的唯一原因是您错误地将 ObjectId 值存储为这些字段中的“字符串”。当存在“真正”的 ObjectId,就像在 _id 字段中一样,比较需要从 valueOf() 中获取“字符串”才能进行比较:

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

没有这个操作符,使用JavaScript进行“对象比较”,{ a: 1 } === { a: 1 }实际上是false。因此,避免这种复杂性是存在本地运算符的另一个原因。

@Ashish 他们通常就像我在这里输入一样,而且这是在早餐时完成的。已更正命名和缺少的大括号。 - Neil Lunn
嗨Neil,感谢您的回复,不过我需要更多帮助。关于第一个查询(使用aggregate()),我看到它返回了owner._id=createdBy._id的结果,而我不想要这个。我还要更新所有结果文档,将owner._id设置为等于createdBy._id(我将编辑我的问题以包括此内容)。我不确定是否可以使用聚合来进行updateMany()。关于接下来的两个(javascript)查询,我仍然收到关于“e.profile未定义”的相同错误消息。 - Kevin
@Kevin 这里的所有示例都没有返回那两个字段匹配的结果,因为它们都有一个明确的条件,说明它们不应该匹配。如果您看到不同的结果,那么您可能在一个字段中有一个“字符串”,而在另一个字段中有一个ObjectId。解决这个问题是一个不同的问题。您可以使用$expr$where进行更新,我已经通过复制您在问题中提供的数据和我的代码来运行查询,一切都正常工作。如果您正在做一些不同的事情,请不要这样做,并建议您也从这里复制文档并运行查询。 - Neil Lunn
@Kevin,既然这里的查询实际上“没有错误”,并且根据问题中提供的实际数据按设计工作,那么您有一种确认并继续前进的方法。如果您有新问题,请提出新问题。但是,这里提出的问题已经得到了充分的回答。实际上,您已经收到了关于ObjectId的详细“注意事项”,因为您的错误似乎非常明显。 - Neil Lunn
事实证明,TypeError发生的原因是因为一些文档具有没有profile字段的acl数组元素。这是我的疏忽,因为我对此集合的模式做出了错误的假设。我会添加一个关于这个问题的注释。谢谢。 - Kevin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接