【问题标题】:Update array of subdocuments in MongoDB更新 MongoDB 中的子文档数组
【发布时间】:2016-12-31 17:43:02
【问题描述】:

我有一组学生,他们有一个名字和一组电子邮件地址。学生文档如下所示:

{
  "_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 数组的结果是:

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

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

【问题讨论】:

  • $addToSet 的问题在于它会忽略重复项,并且在使用已存在的标签发布电子邮件值时不会替换现有值。但是,如果标签已经存在,我希望子文档被替换/更新。
  • 糟糕,在没有完全阅读问题的情况下就在这里开枪了。是的,你是对的,在这种情况下它不起作用。
  • @chridam 没问题。还有其他想法吗?

标签: arrays mongodb mongoose updates


【解决方案1】:

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

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

【讨论】:

  • 有趣的方法。我想通过使用 lodash 的 unionBy() lodash.com/docs#unionBy 在代码中进行匹配 - 然后只需 $set 新数组。我认为它会有更好的性能。
【解决方案2】:

建议:重构您的数据以使用“标签”作为实际字段名称。

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 对象的工作方式会自动满足您的数据要求。

【讨论】:

  • 我也想过这个。我看到的问题是用户无法定义他想要的任何标签。我必须提前定义字段的名称——至少在我使用 mongoose 时。我能想到的唯一方法是定义混合模式(mongoosejs.com/docs/schematypes.html)的 emails 属性,但这将是我无法对数据运行任何验证的缺点。
  • 仅供将来参考:刚刚发现实现此目的的另一种方法是为电子邮件定义自定义架构,然后在该架构上设置 strict:false 选项。这样,您可以定义某些属性,但仍然可以添加新属性。然而,对于未定义的属性,问题仍然是没有验证。见:github.com/Automattic/mongoose/issues/2010
【解决方案3】:

在调查了发布的不同选项后,我决定采用自己的方法,使用 lodash 的 unionBy() 函数在代码中手动进行更新。使用 express 和 mongoose 的 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 选项,您将允许用户也发布带有自定义标签的电子邮件。但是请注意,这些不会被验证。您必须在应用程序中手动应用验证,类似于我在上述合并方法中所做的方式。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-03-30
    • 2020-10-02
    • 1970-01-01
    • 2017-08-22
    • 2012-11-26
    • 1970-01-01
    • 2016-11-23
    • 2016-11-04
    相关资源
    最近更新 更多