如何在Mongoose中更新/上传文档?


367

也许是时候了,也许是我淹没在稀疏的文档中,无法将自己的头围在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{...})
    我会陷入某种无限循环,因为Mongoose更新实现显然不希望将对象作为第二个参数。
  4. 如果执行相同的操作,但是在第二个参数中,我传递了一个有效的请求属性的关联数组{status: request.status, phone: request.phone ...}-但随后我没有对特定联系人的引用,并且无法找到其createdAtupdatedAt属性。

因此,在尝试了所有操作之后,底线是:给定了文档contact,如果文档存在,如何更新;如果文档不存在,如何添加?

谢谢你的时间。


钩上prefor save呢?
Shamoon

Answers:


427

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不支持使用以下方法的这些钩子:

  • 默认值
  • 二传手
  • 验证者
  • 中间件

17
这应该是最新的答案。大多数其他用途有两个调用,或者(我相信)会回退到本机mongodb驱动程序。
huggie 2014年

10
findOneAndUpdate的问题是不会执行预保存。
a77icu5 2014年

2
听起来像是Mongoose或MongoDB中的错误?
Pascalius 2014年

8
从文档:“......使用findAndModify助手时,以下是不适用:违约,制定者,验证,中间件” mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
科伦

2
@JamieHutber默认情况下未设置,这是一个自定义属性
Pascalius

194

我只花了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项目页面上了一个问题,要求将有关此信息的信息添加到文档中。


1
目前,文档似乎不多。API文档中有一些内容(在页面上搜索“ update”。看起来像这样:MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
CpILL 2012年

对于找不到的案例文件,使用哪个_id?猫鼬会生成它还是遭到查询的对象?
Haider

91

你很亲近

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

但是您的第二个参数应该是带有修改运算符的对象,例如

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

15
我认为您不需要{$set: ... }此处作为自动阅读的部分
CpILL,2012年

5
是的,猫鼬说它将一切变成$ set
Grantwparks 2012年

1
这在撰写本文时是有效的,我不再使用MongoDB,所以我不能谈近月的变化:D
chrixian 2012年

5
如果不时使用本机驱动程序,则不使用$ set可能是一个坏习惯。
UpTheCreek 2014年

在插入的情况下,可以使用$ set和$ setOnInsert仅设置某些字段
justin.m.chase

73

好吧,我等待了足够长的时间,没有答案。最后放弃了整个更新/更新方法,并进行了以下操作:

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功能。


2
本示例使用MongoDB 2.2中添加的接口以文档形式指定multi和upsert选项。.. include :: /includes/fact-upsert-multi-options.rst文档正在说明这一点,不知道从这里开始。
唐纳德·德里克

1
尽管这应该可行,但是现在只需要1(向上更新)就可以运行2个操作(查找,更新)。@chrixian显示了执行此操作的正确方法。
尊重TheCode 2013年

12
值得注意的是,这是允许Mongoose的验证器加入的唯一答案。根据docs,如果您调用update,则不会进行验证。
汤姆·斯宾塞

@fiznool貌似可以在选项通过手动runValidators: true更新过程中:更新文档(但是,更新验证只在运行$set$unset操作)
丹尼

如果您需要.upsert()在所有型号上都可用,请参阅基于此的答案。stackoverflow.com/a/50208331/1586406
spondbob

24

您可以通过使用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);
    });
});

为什么不赞成?似乎是一个很好的解决方案,而且非常优雅
MadOgre

出色的解决方案实际上使我重新考虑了如何兑现承诺。
勒克斯

4
更优雅的将改写(model) => { return model.save(); }model => model.save(),也(err) => { res.send(err); }作为err => res.send(err);)
杰里米Thille

1
在哪里可以获得更多信息?
V0LT3RR4

17

我是猫鼬的维护者。更新文档的更现代方法是使用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 });

关键要点是,您需要将filter参数中的唯一属性放入updateOne()findOneAndUpdate(),并将其他属性放入update参数中。

这是有关使用Mongoose升级文档的教程。


15

我创建了一个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()
  • 您可以在更新调用中向上插入深层嵌套的对象而无需手动枚举,因此,如果模型发生更改,则不必担心更新代码

只要记住,即使源具有现有值,目标对象也将始终覆盖源。

同样,对于数组,如果现有对象的数组比替换对象的数组长,则旧数组末尾的值将保留。向上插入整个数组的一种简单方法是,如果您打算这样做,则在插入之前将旧数组设置为空数组。

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


2
+1为创建acc仅用于此:P希望我可以再给一个+1以仅用于使用.save(),因为findOneAndUpate()使我们无法使用验证器以及前置,后置等内容。谢谢,我也会检查一下
user1576978

抱歉,但是在这里不起作用:(我超出了调用堆栈的大小
user1576978

您正在使用什么版本的lodash?我正在使用lodash版本2.4.1谢谢!
亚伦桅杆

另外,您要增加对象的复杂程度吗?如果它们太大,则节点进程可能无法处理合并对象所需的递归调用数。
亚伦桅杆

我使用了此方法,但必须添加if(_.isObject(value) && _.keys(value).length !== 0) {保护条件以停止堆栈溢出。Lodash 4+在这里,似乎在keys调用中将非对象值转换为对象,因此递归保护始终为真。也许有更好的方法,但是它现在几乎对我有用...
理查德·G

12

我需要将文档更新/更新到一个集合中,我要做的是创建一个像这样的新对象文字:

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


10
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了解更多详细信息。这绝对对我有用!!!


5
认为这与MongoDB的更新具有相同的作用是错误的。这不是原子的。
Valentin Waeselynck

1
我想备份@ValentinWaeselynck答案。Scotch的代码很干净-但是您正在获取文档,然后进行更新。在该过程的中间,可以更改文档。
尼克·皮内达

8

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

7

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

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

5

这对我有用。

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

});


谢谢。这是最终为我工作的那个!
Luis Febro

4

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

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

3

对于到这里仍在寻找具有钩子支持的“插入”好的解决方案的任何人,这就是我已经测试和工作的东西。它仍然需要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);
    });
  });
}

3

如果有发电机,它将变得更加容易:

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

3

没有其他解决方案对我有用。我正在使用发布请求并更新数据(如果找到的话),否则将其插入,同时_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');

});

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


--

2
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
    if(err) return res.json(err);

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

尽管此代码段可以解决问题,但并未说明原因或答案。请为您的代码提供说明,因为这确实有助于提高您的帖子质量。请记住,您将来会为读者回答这个问题,而这些人可能不知道您提出代码建议的原因。 举报人/审阅者: 对于仅限代码的答案,例如这一答案,请表决,不要删除!
帕特里克

2

按照Traveling Tech Guy的回答(已经很棒),我们可以创建一个插件并将其初始化后附加到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 })只要你想。



0

这个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

0

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

1
req.body在进行NoSQL注入测试之前,我警告不要按原样使用(请参阅owasp.org/index.php/Testing_for_NoSQL_injection)。
Traveling Tech Guy's

1
@TravelingTechGuy感谢您的谨慎,我还是Node和Mongoose的新手。我的带有验证器的猫鼬模型不足以捕捉注射尝试吗?在model.save()期间
克里斯·

-5

阅读以上内容后,我决定使用此代码:

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


如果是打给Mongo的两个电话,那不是真的很重要吗?
huggie 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.