更新 MongoDB 中的子文档数组

Update array of subdocuments in MongoDB

我有一组学生,他们有一个名字和一组电子邮件地址。学生文件看起来像这样:

{
  "_id": {"$oid": "56d06bb6d9f75035956fa7ba"},
  "name": "John Doe",
  "emails": [
    {
      "label": "private",
      "value": "private@johndoe.com"
    },
    {
      "label": "work",
      "value": "work@johndoe.com"
    }
  ]
}

电子邮件子文档中的标签设置为每个文档都是唯一的,因此不能有两个条目具有相同的标签。

我的问题是,在更新学生文档时,我想实现以下目标:

例如,使用以下数据更新时:

{
  "_id": {"$oid": "56d06bb6d9f75035956fa7ba"},
  "emails": [
    {
      "label": "private",
      "value": "me@johndoe.com"
    },
    {
      "label": "school",
      "value": "school@johndoe.com"
    }
  ]
}

我希望电子邮件数组的结果是:

"emails": [
    {
      "label": "private",
      "value": "me@johndoe.com"
    },
    {
      "label": "work",
      "value": "work@johndoe.com"
    },
    {
      "label": "school",
      "value": "school@johndoe.com"
    }
  ]

如何在 MongoDB 中实现这一点(可选地使用 mongoose)?这是完全可能的还是我必须在应用程序代码中自己检查数组?

您可以尝试此更新,但仅对小型数据集有效:

mongo shell:

var data = {
    "_id": ObjectId("56d06bb6d9f75035956fa7ba"),
    "emails": [
        {
          "label": "private",
          "value": "me@johndoe.com"
        },
        {
          "label": "school",
          "value": "school@johndoe.com"
        }
    ]
};

data.emails.forEach(function(email) {
    var emails = db.students.findOne({_id: data._id}).emails,
        query = { "_id": data._id },
        update = {};

    emails.forEach(function(e) {
        if (e.label === email.label) {
            query["emails.label"] = email.label;
            update["$set"] = { "emails.$.value": email.value };
        } else {
           update["$addToSet"] = { "emails": email }; 
        }
        db.students.update(query, update)
    });
});

建议:重构您的数据以使用 "label" 作为实际字段名称。

有一种直接的方法 MongoDB 可以保证给定电子邮件标签的唯一值 - 通过在电子邮件子文档中使标签本身成为一个单独的字段。您的数据需要存在于此结构中:

{
  "_id": ObjectId("56d06bb6d9f75035956fa7ba"),
  "name": "John Doe",
  "emails": {
      "private": "private@johndoe.com",
      "work" : "work@johndoe.com"
  }
}

现在,当您想更新学生的电子邮件时,您可以像这样进行更新:

db.students.update(
    {"_id": ObjectId("56d06bb6d9f75035956fa7ba")},
    {$set: {
        "emails.private" : "me@johndoe.com",
        "emails.school" : "school@johndoe.com"
    }}
);

这会将数据更改为:

{
  "_id": ObjectId("56d06bb6d9f75035956fa7ba"),
  "name": "John Doe",
  "emails": {
      "private": "me@johndoe.com",
      "work" : "work@johndoe.com",
      "school" : "school@johndoe.com"
  }
}

不可否认,这种方法有一个缺点:您将需要更改输入数据的结构,从子文档数组中的电子邮件到包含单个字段的单个子文档的电子邮件。但优点是 JSON 对象的工作方式会自动满足您的数据要求。

在研究了不同的选项 posted 之后,我决定采用我自己的方法,使用 lodash 的 unionBy() function. Using express and mongoose's findById() 在代码中手动进行更新,基本上如下所示:

Student.findById(req.params.id, function(err, student) {

    if(req.body.name) student.name = req.body.name;
    if(req.body.emails && req.body.emails.length > 0) {
        student.emails = _.unionBy(req.body.emails, student.emails, 'label');
    }

    student.save(function(err, result) {
        if(err) return next(err);
        res.status(200).json(result);
    });

});

这样我就可以完全灵活地对所有字段进行部分更新。当然你也可以使用 findByIdAndUpdate() 或其他选项。

替代方法:

然而,像 Vince Bowdren 所建议的那样改变模式的方式,使 label 成为电子邮件子文档中的一个单独的字段,也是一个可行的选择。最后还是看个人喜好,是否需要对数据进行严格验证。

如果您像我一样使用猫鼬,则必须像这样定义一个单独的模式:

var EmailSchema = new mongoose.Schema({
  work: { type: String, validate: validateEmail },
  private: { type: String, validate: validateEmail }
}, {
  strict: false,
  _id: false
});

在架构中,您可以为已经希望支持的标签定义属性并添加验证。通过设置 strict: false 选项,您将允许用户还 post 带有自定义标签的电子邮件。但是请注意,这些不会被验证。您必须在您的应用程序中手动应用验证,类似于我在上面的合并方法中所做的方式。