我有一个学生集合,其中有一个名字和一个电子邮件地址数组。学生文档看起来像这样:
{
"_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对象的工作方式自动满足您的数据需求。
在研究了发布的不同选项之后,我决定使用我自己的方法,使用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
在电子邮件子文档中成为一个单独的字段,也是一个可行的选择。最后,这取决于你的个人喜好,以及你是否需要严格验证你的数据。
如果你像我一样使用mongoose,你必须定义一个单独的模式,像这样:
var EmailSchema = new mongoose.Schema({
work: { type: String, validate: validateEmail },
private: { type: String, validate: validateEmail }
}, {
strict: false,
_id: false
});
在模式中,您可以为已经想要支持的标签定义属性并添加验证。通过设置strict: false
选项,您将允许用户也发布带有自定义标签的电子邮件。但是请注意,这些不会被验证。您必须在您的应用程序中手动应用验证,就像我在上面的合并方法中所做的那样。