如何在 Mongoose 中更新/插入文档?
Posted
技术标签:
【中文标题】如何在 Mongoose 中更新/插入文档?【英文标题】:How do I update/upsert a document in Mongoose? 【发布时间】:2011-11-08 04:30:28 【问题描述】:也许是时候了,也许是我淹没在稀疏的文档中,无法理解在 Mongoose 中更新的概念 :)
这是交易:
我有一个联系人架构和模型(缩短的属性):
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var mongooseTypes = require("mongoose-types"),
useTimestamps = mongooseTypes.useTimestamps;
var ContactSchema = new Schema(
phone:
type: String,
index:
unique: true,
dropDups: true
,
status:
type: String,
lowercase: true,
trim: true,
default: 'on'
);
ContactSchema.plugin(useTimestamps);
var Contact = mongoose.model('Contact', ContactSchema);
我收到来自客户的请求,其中包含我需要的字段并因此使用我的模型:
mongoose.connect(connectionString);
var contact = new Contact(
phone: request.phone,
status: request.status
);
现在我们遇到了问题:
-
如果我拨打
contact.save(function(err)...)
,如果已存在具有相同电话号码的联系人(如预期 - 唯一),我将收到错误消息
我无法联系update()
,因为文档中不存在该方法
如果我在模型上调用更新:Contact.update(phone:request.phone, contact, upsert: true, function(err...)
我进入了某种无限循环,因为 Mongoose 更新实现显然不希望将对象作为第二个参数。
如果我也这样做,但在第二个参数中我传递了请求属性的关联数组status: request.status, phone: request.phone ...
它可以工作 - 但是我没有参考具体的联系人,也找不到它的createdAt
和@987654329 @ 属性。
所以底线,毕竟我尝试过:给定一个文档contact
,如果它存在,我该如何更新它,或者如果它不存在,我该如何添加它?
感谢您的宝贵时间。
【问题讨论】:
在pre
中为save
挂钩怎么样?
mongoosejs.com/docs/documents.html
【参考方案1】:
好吧,我等了足够长的时间,但没有答案。最后放弃了整个更新/更新方法并选择了:
ContactSchema.findOne(phone: request.phone, function(err, contact)
if(!err)
if(!contact)
contact = new ContactSchema();
contact.phone = request.phone;
contact.status = request.status;
contact.save(function(err)
if(!err)
console.log("contact " + contact.phone + " created at " + contact.createdAt + " updated at " + contact.updatedAt);
else
console.log("Error: could not save contact " + contact.phone);
);
);
有效吗?是的。我对此满意吗?可能不是。 2 个数据库调用,而不是 1 个。
希望未来的 Mongoose 实现能够提供 Model.upsert
函数。
【讨论】:
此示例使用 MongoDB 2.2 中添加的接口在文档表单中指定 multi 和 upsert 选项。 .. include:: /includes/fact-upsert-multi-options.rst 文档说明了这一点,不知道从哪里开始。 虽然这应该可行,但您现在正在运行 2 个操作(查找、更新),而只需要 1 个(更新插入)。 @chrixian 展示了正确的方法。 值得注意的是,这是允许 Mongoose 的验证器启动的唯一答案。As per the docs,如果您调用 update 则不会发生验证。 @fiznool 看起来您可以在更新期间手动传入选项runValidators: true
:update docs(但是,更新验证器仅在 $set
和 $unset
操作上运行)
如果您需要 .upsert()
在所有型号上可用,请查看我基于此的答案。 ***.com/a/50208331/1586406【参考方案2】:
你很亲密
Contact.update(phone:request.phone, contact, upsert: true, function(err)...)
但您的第二个参数应该是一个带有修改运算符的对象,例如
Contact.update(phone:request.phone, $set: phone: request.phone , upsert: true, function(err)...)
【讨论】:
我认为您不需要这里的$set: ...
部分作为我阅读的自动形式
是的,猫鼬说它把所有东西都变成了 $set
这在撰写本文时是有效的,我不再使用 MongoDB,所以我无法谈论最近几个月的变化:D
如果您不时使用本机驱动程序,则不使用 $set 可能是一个坏习惯。
在插入的情况下可以使用 $set 和 $setOnInsert 只设置某些字段【参考方案3】:
我刚刚花了 3 个小时来尝试解决同样的问题。具体来说,我想“替换”整个文档(如果存在),否则插入它。这是解决方案:
var contact = new Contact(
phone: request.phone,
status: request.status
);
// Convert the Model instance to a simple object using Model's 'toObject' function
// to prevent weirdness like infinite looping...
var upsertData = contact.toObject();
// Delete the _id property, otherwise Mongo will return a "Mod on _id not allowed" error
delete upsertData._id;
// Do the upsert, which works like this: If no Contact document exists with
// _id = contact.id, then create a new doc using upsertData.
// Otherwise, update the existing doc with upsertData
Contact.update(_id: contact.id, upsertData, upsert: true, function(err...);
我创建了an issue on the Mongoose project page,请求将有关此的信息添加到文档中。
【讨论】:
目前文档似乎很差。 API文档中有一些(在页面上搜索“更新”。看起来像这样:MyModel.update( age: $gt: 18 , oldEnough: true , fn);
和MyModel.update( name: 'Tobi' , ferret: true , multi: true , fn);
对于没有找到案例文档,使用哪个_id? Mongoose 生成它还是被查询的那个?【参考方案4】:
看完上面的帖子,我决定用这个代码:
itemModel.findOne('pid':obj.pid,function(e,r)
if(r!=null)
itemModel.update('pid':obj.pid,obj,upsert:true,cb);
else
var item=new itemModel(obj);
item.save(cb);
);
如果 r 为空,我们创建新项目。否则,请在更新中使用 upsert,因为更新不会创建新项目。
【讨论】:
如果是对 Mongo 的两次调用,那不是真的 upsert 是吗?【参考方案5】:我需要将文档更新/插入到一个集合中,我所做的是创建一个新的对象字面量,如下所示:
notificationObject =
user_id: user.user_id,
feed:
feed_id: feed.feed_id,
channel_id: feed.channel_id,
feed_title: ''
;
由我从数据库中其他地方获取的数据组成,然后在模型上调用更新
Notification.update(notificationObject, notificationObject, upsert: true, function(err, num, n)
if(err)
throw err;
console.log(num, n);
);
这是我第一次运行脚本后得到的输出:
1 updatedExisting: false,
upserted: 5289267a861b659b6a00c638,
n: 1,
connectionId: 11,
err: null,
ok: 1
这是我第二次运行脚本时的输出:
1 updatedExisting: true, n: 1, connectionId: 18, err: null, ok: 1
我使用的是 mongoose 版本 3.6.16
【讨论】:
【参考方案6】:这个coffeescript适用于Node - 诀窍是_id get在从客户端发送和返回时剥离其ObjectID包装器,因此需要更换更新(当没有提供_id时,保存将恢复为插入并添加一个)。
app.post '/new', (req, res) ->
# post data becomes .query
data = req.query
coll = db.collection 'restos'
data._id = ObjectID(data._id) if data._id
coll.save data, safe:true, (err, result) ->
console.log("error: "+err) if err
return res.send 500, err if err
console.log(result)
return res.send 200, JSON.stringify result
【讨论】:
【参考方案7】:在 2.6 中引入了一个错误,对 2.7 也有影响
upsert 过去在 2.4 上可以正常工作
https://groups.google.com/forum/#!topic/mongodb-user/UcKvx4p4hnY https://jira.mongodb.org/browse/SERVER-13843
看一下,里面有一些重要信息
更新:
这并不意味着 upsert 不起作用。这是一个如何使用它的好例子:
User.findByIdAndUpdate(userId, online: true, $setOnInsert: username: username, friends: [], upsert: true)
.populate('friends')
.exec(function (err, user)
if (err) throw err;
console.log(user);
// Emit load event
socket.emit('load', user);
);
【讨论】:
【参考方案8】:Mongoose 现在通过 findOneAndUpdate(调用 MongoDB findAndModify)本机支持此功能。
如果对象不存在,upsert = true 选项会创建该对象。 默认为 false。
var query = 'username': req.user.username;
req.newData.username = req.user.username;
MyModel.findOneAndUpdate(query, req.newData, upsert: true, function(err, doc)
if (err) return res.send(500, error: err);
return res.send('Succesfully saved.');
);
在旧版本中,Mongoose 不支持使用此方法的这些钩子:
默认值 二传手 验证器 中间件【讨论】:
这应该是最新的答案。大多数其他人使用两个调用或(我相信)回退到本机 mongodb 驱动程序。 findOneAndUpdate 的问题是不会执行预保存。 听起来像是 Mongoose 或 MongoDB 中的错误? 来自文档:“...使用 findAndModify 助手时,以下内容不适用:默认值、设置器、验证器、中间件”mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate @JamieHutber 这个默认没有设置,是自定义属性【参考方案9】:对于到达这里的任何人仍在寻找具有钩子支持的“更新插入”的良好解决方案,这就是我已经测试和工作的内容。它仍然需要 2 次 DB 调用,但比我在单个调用中尝试过的任何方法都要稳定。
// Create or update a Person by unique email.
// @param person - a new or existing Person
function savePerson(person, done)
var fieldsToUpdate = ['name', 'phone', 'address'];
Person.findOne(
email: person.email
, function(err, toUpdate)
if (err)
done(err);
if (toUpdate)
// Mongoose object have extra properties, we can either omit those props
// or specify which ones we want to update. I chose to update the ones I know exist
// to avoid breaking things if Mongoose objects change in the future.
_.merge(toUpdate, _.pick(person, fieldsToUpdate));
else
toUpdate = person;
toUpdate.save(function(err, updated, numberAffected)
if (err)
done(err);
done(null, updated, numberAffected);
);
);
【讨论】:
【参考方案10】://Here is my code to it... work like ninj
router.param('contractor', function(req, res, next, id)
var query = Contractors.findById(id);
query.exec(function (err, contractor)
if (err) return next(err);
if (!contractor) return next(new Error("can't find contractor"));
req.contractor = contractor;
return next();
);
);
router.get('/contractors/:contractor/save', function(req, res, next)
contractor = req.contractor ;
contractor.update('_id':contractor._id,upsert: true,function(err,contractor)
if(err)
res.json(err);
return next();
return res.json(contractor);
);
);
--
【讨论】:
【参考方案11】:我创建了一个 *** 帐户只是为了回答这个问题。在无果地搜索互联网之后,我自己写了一些东西。我就是这样做的,因此它可以应用于任何猫鼬模型。导入此函数或将其直接添加到您正在进行更新的代码中。
function upsertObject (src, dest)
function recursiveFunc (src, dest)
_.forOwn(src, function (value, key)
if(_.isObject(value) && _.keys(value).length !== 0)
dest[key] = dest[key] || ;
recursiveFunc(src[key], dest[key])
else if (_.isArray(src) && !_.isObject(src[key]))
dest.set(key, value);
else
dest[key] = value;
);
recursiveFunc(src, dest);
return dest;
然后要更新 mongoose 文档,请执行以下操作,
YourModel.upsert = function (id, newData, callBack)
this.findById(id, function (err, oldData)
if(err)
callBack(err);
else
upsertObject(newData, oldData).save(callBack);
);
;
此解决方案可能需要 2 次 DB 调用,但您确实会从中受益,
针对您的模型进行架构验证,因为您使用的是 .save() 您可以在更新调用中插入深度嵌套的对象而无需手动枚举,因此如果您的模型发生更改,您不必担心更新代码请记住,目标对象将始终覆盖源,即使源具有现有值
另外,对于数组,如果现有对象的数组比替换它的数组长,则旧数组末尾的值将保留。 upsert 整个数组的一种简单方法是在 upsert 之前将旧数组设置为空数组,如果这是您打算这样做的话。
更新 - 2016 年 1 月 16 日 我添加了一个额外条件,如果存在原始值数组,Mongoose 不会意识到该数组会在不使用“set”函数的情况下更新。
【讨论】:
+1 用于为此创建 acc :P 希望我可以给另一个 +1 仅用于使用 .save(),因为 findOneAndUpate() 使我们无法使用验证器和 pre、post 等东西.谢谢,我也去看看 抱歉,这里不起作用:(我的调用堆栈大小超出了 您使用的是什么版本的 lodash?我正在使用 lodash 版本 2.4.1 谢谢! 另外,你更新插入的对象有多复杂?如果它们太大,节点进程可能无法处理合并对象所需的递归调用次数。 我使用了这个,但必须在保护条件上添加if(_.isObject(value) && _.keys(value).length !== 0)
以阻止堆栈溢出。 Lodash 4+ 在这里,它似乎将非对象值转换为 keys
调用中的对象,因此递归保护总是正确的。也许有更好的方法,但它现在几乎对我有用......【参考方案12】:
如果有生成器,它会变得更加容易:
var query = 'username':this.req.user.username;
this.req.newData.username = this.req.user.username;
this.body = yield MyModel.findOneAndUpdate(query, this.req.newData).exec();
【讨论】:
【参考方案13】:app.put('url', function(req, res)
// use our bear model to find the bear we want
Bear.findById(req.params.bear_id, function(err, bear)
if (err)
res.send(err);
bear.name = req.body.name; // update the bears info
// save the bear
bear.save(function(err)
if (err)
res.send(err);
res.json( message: 'Bear updated!' );
);
);
);
这里有一个更好的解决猫鼬更新方法的方法,您可以查看Scotch.io了解更多详细信息。这绝对对我有用!!!
【讨论】:
认为这与 MongoDB 的更新做同样的事情是错误的。它不是原子的。 我想备份@ValentinWaeselynck 的答案。 Scotch 的代码很干净——但您正在获取文档然后进行更新。在该过程的中间,文档可能会被更改。【参考方案14】:使用 Promise 链可以实现非常优雅的解决方案:
app.put('url', (req, res) =>
const modelId = req.body.model_id;
const newName = req.body.name;
MyModel.findById(modelId).then((model) =>
return Object.assign(model, name: newName);
).then((model) =>
return model.save();
).then((updatedModel) =>
res.json(
msg: 'model updated',
updatedModel
);
).catch((err) =>
res.send(err);
);
);
【讨论】:
为什么没有投票?似乎是一个很好的解决方案,而且非常优雅 出色的解决方案,实际上让我重新思考了我如何处理承诺。 更优雅的是将(model) => return model.save();
重写为model => model.save()
,并将(err) => res.send(err);
重写为err => res.send(err)
;)【参考方案15】:
一段时间后我才回到这个问题,并决定根据 Aaron Mast 的回答发布一个插件。
https://www.npmjs.com/package/mongoose-recursive-upsert
将其用作猫鼬插件。它设置了一个静态方法,将递归合并传入的对象。
Model.upsert(unique: 'value', updateObject);
【讨论】:
【参考方案16】:以 Martin Kuzdowicz 上面发布的内容为基础。我使用以下内容使用猫鼬和 json 对象的深度合并进行更新。与 mongoose 中的 model.save() 函数一起,这允许 mongoose 进行完全验证,即使是依赖于 json 中其他值的验证。它确实需要 deepmerge 包https://www.npmjs.com/package/deepmerge。但这是一个重量很轻的包装。
var merge = require('deepmerge');
app.put('url', (req, res) =>
const modelId = req.body.model_id;
MyModel.findById(modelId).then((model) =>
return Object.assign(model, merge(model.toObject(), req.body));
).then((model) =>
return model.save();
).then((updatedModel) =>
res.json(
msg: 'model updated',
updatedModel
);
).catch((err) =>
res.send(err);
);
);
【讨论】:
在测试 NoSQL 注入(请参阅 owasp.org/index.php/Testing_for_NoSQL_injection)之前,请注意不要按原样使用req.body
。
@TravelingTechGuy 感谢您的谨慎,我还是 Node 和 Mongoose 的新手。我的带有验证器的猫鼬模型是否足以捕获注入尝试?在model.save()【参考方案17】:
这是创建/更新同时调用中间件和验证器的最简单方法。
Contact.findOne( phone: request.phone , (err, doc) =>
const contact = (doc) ? doc.set(request) : new Contact(request);
contact.save((saveErr, savedContact) =>
if (saveErr) throw saveErr;
console.log(savedContact);
);
)
【讨论】:
【参考方案18】:User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) =>
if(err) return res.json(err);
res.json( success: true );
);
【讨论】:
虽然这段代码 sn-p 可以解决问题,但它没有解释为什么或如何回答这个问题。请include an explanation for your code,因为这确实有助于提高您的帖子质量。请记住,您正在为将来的读者回答问题,而这些人可能不知道您的代码建议的原因。 举报者/评论者: For code-only answers such as this one, downvote, don't delete!【参考方案19】:没有其他解决方案适合我。我正在使用发布请求并更新数据,如果发现其他插入它,_id 也会与需要删除的请求正文一起发送。
router.post('/user/createOrUpdate', function(req,res)
var request_data = req.body;
var userModel = new User(request_data);
var upsertData = userModel.toObject();
delete upsertData._id;
var currentUserId;
if (request_data._id || request_data._id !== '')
currentUserId = new mongoose.mongo.ObjectId(request_data._id);
else
currentUserId = new mongoose.mongo.ObjectId();
User.update(_id: currentUserId, upsertData, upsert: true,
function (err)
if (err) throw err;
);
res.redirect('/home');
);
【讨论】:
【参考方案20】:这对我有用。
app.put('/student/:id', (req, res) =>
Student.findByIdAndUpdate(req.params.id, req.body, (err, user) =>
if (err)
return res
.status(500)
.send(error: "unsuccessful")
;
res.send(success: "success");
);
);
【讨论】:
谢谢。这是最终为我工作的那个! @Emmanuel 这对我来说也很好用,但是如果我们仔细观察这是在更新记录的 id,知道如何避免更新 id 吗?【参考方案21】:根据Traveling Tech Guy 的回答,这已经很棒了,我们可以创建一个插件,并在初始化后将其附加到猫鼬,这样.upsert()
将适用于所有型号。
plugins.js
export default (schema, options) =>
schema.statics.upsert = async function(query, data)
let record = await this.findOne(query)
if (!record)
record = new this(data)
else
Object.keys(data).forEach(k =>
record[k] = data[k]
)
return await record.save()
db.js
import mongoose from 'mongoose'
import Plugins from './plugins'
mongoose.connect( ... )
mongoose.plugin(Plugins)
export default mongoose
然后您可以随时执行User.upsert( _id: 1 , foo: 'bar' )
或YouModel.upsert( bar: 'foo' , value: 1 )
之类的操作。
【讨论】:
【参考方案22】:您可以简单地用这个更新记录并获取更新的数据作为响应
router.patch('/:id', (req, res, next) =>
const id = req.params.id;
Product.findByIdAndUpdate(id, req.body,
new: true
,
function(err, model)
if (!err)
res.status(201).json(
data: model
);
else
res.status(500).json(
message: "not found any relative data"
)
);
);
【讨论】:
@Awais/@Andrew 这段代码实际上也在更新我的 id,关于如何摆脱 id 更新的任何想法。【参考方案23】:我是 Mongoose 的维护者。更新文档的更现代方式是使用Model.updateOne()
function。
await Contact.updateOne(
phone: request.phone
, status: request.status , upsert: true );
如果您需要插入的文档,可以使用Model.findOneAndUpdate()
const doc = await Contact.findOneAndUpdate(
phone: request.phone
, status: request.status , upsert: true, useFindAndModify: false );
关键点是您需要将filter
参数中的唯一属性放入updateOne()
或findOneAndUpdate()
,并将其他属性放入update
参数中。
这是upserting documents with Mongoose的教程。
【讨论】:
这不会跳过模型验证或“预”中间件吗?以上是关于如何在 Mongoose 中更新/插入文档?的主要内容,如果未能解决你的问题,请参考以下文章