我如何更新/upsert一个文档在猫鼬?

也许是时间的问题,也许是我淹没在稀疏的文档中,无法理解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
});

现在问题来了:

  1. 如果我调用contact.save(function(err){...}),如果具有相同电话号码的联系人已经存在,我会收到一个错误(正如预期的-唯一)
  2. 我不能在联系人上调用update(),因为该方法不存在于文档中
  3. 如果我对模型调用update:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...}) < br > 我进入了某种类型的无限循环,因为Mongoose更新实现显然不希望将对象作为第二个参数
  4. 如果我做同样的事情,但在第二个参数中,我传递了一个请求属性{status: request.status, phone: request.phone ...}的关联数组,它可以工作-但是我没有对特定联系人的引用,无法找到它的createdAtupdatedAt属性。

所以底线,毕竟我尝试:给定一个文档contact,我如何更新它,如果它存在,或添加它,如果它不存在?

谢谢你的时间。

652581 次浏览

我等了很久,还是没有回音。最后放弃了整个更新/upsert方法,并使用:

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个DB调用而不是一个 希望将来的Mongoose实现能够提供Model.upsert函数

你曾经和

Contact.update({phone:request.phone}, contact, {upsert: true}, function(err){...})

但是第二个参数应该是一个带有修改操作符的对象

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})

我花了整整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{...});

我创建了Mongoose项目页面上的问题,请求将此信息添加到文档中。

在阅读了上面的文章之后,我决定使用下面的代码:

    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为空,则创建新项。否则,在update中使用upsert,因为update不会创建新项。

我需要更新/upsert一个文档到一个集合,我所做的是创建一个新的对象文字如下:

notificationObject = {
user_id: user.user_id,
feed: {
feed_id: feed.feed_id,
channel_id: feed.channel_id,
feed_title: ''
}
};

由我从数据库中其他地方获得的数据组成,然后在模型上调用update

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 }

我使用的是猫鼬3.6.16版本

这个coffeescript为我与节点工作-技巧是_id获取剥离了它的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

2.6引入了一个bug,影响到2.7

upsert过去在2.4上正常工作

< a href = " https://groups.google.com/forum/ # ! / mongodb-user / UcKvx4p4hnY”主题> https://groups.google.com/forum/ !主题/ mongodb-user / UcKvx4p4hnY https://jira.mongodb.org/browse/SERVER-13843 < / p >

看一下,它包含了一些重要的信息

更新:

这并不意味着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);
});

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不支持这些钩子:

  • 违约
  • setter
  • 验证器
  • 中间件

对于任何到达这里仍然在寻找一个好的解决方案“upserting”与钩子支持,这是我已经测试和工作。它仍然需要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);
});
});
}
//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);
});
});




--

我创建了一个StackOverflow账户就是为了回答这个问题。在网上毫无结果的搜索之后,我自己写了一些东西。我就是这么做的,所以它可以应用到任何猫鼬模型。要么导入这个函数,要么直接将它添加到您正在进行更新的代码中。

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

然后按以下步骤插入猫鼬文档,

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()
  • 您可以在更新调用中插入深度嵌套的对象,而无需手动枚举,因此如果您的模型更改,您不必担心更新代码

只要记住,目标对象将总是覆盖源,即使源有一个现有的值

同样,对于数组,如果现有对象的数组比替换它的数组长,那么旧数组末尾的值将保持不变。上插入整个数组的一个简单方法是在上插入之前将旧数组设置为空数组(如果这是您打算做的)。

UPDATE - 01/16/2016 我添加了一个额外的条件,如果有一个原始值的数组,Mongoose不会意识到数组在不使用“set”函数的情况下被更新

如果生成器可用,这将变得更加简单:

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();
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更多的细节。这绝对对我有用!!

非常优雅的解决方案,你可以通过使用chain of Promises实现:

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

我只是在一段时间后回到这个问题上,并决定基于Aaron Mast的答案发布一个插件。

https://www.npmjs.com/package/mongoose-recursive-upsert

将其作为猫鼬插件使用。它设置一个静态方法,递归地合并传入的对象。

Model.upsert({unique: 'value'}, updateObject});

以Martin Kuzdowicz上面的帖子为基础。我使用以下内容使用mongoose和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);
});
});

下面是创建/更新同时调用中间件和验证器的最简单方法。

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);
});
})
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
if(err) return res.json(err);


res.json({ success: true });
});

没有其他的解决方案对我有效。我使用post请求和更新数据,如果发现其他插入它,_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');


});

这对我很管用。

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


});

根据旅行科技男的答案,这已经很棒了,我们可以创建一个插件,并在初始化它后将其附加到mongoose,以便.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 })之类的操作。

您可以简单地用它更新记录,并获得更新后的数据作为响应

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

我是猫鼬的维护者。插入文档的更现代的方法是使用Model.updateOne()函数

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参数中。

这里有一个关于用Mongoose上传文件的教程。