【问题标题】:How do I update/upsert a document in Mongoose?如何在 Mongoose 中更新/插入文档?
【发布时间】:2018-01-02 10:16:12
【问题描述】:

也许是时候了,也许是我淹没在稀疏的文档中,无法理解在 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. 如果我调用模型更新:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    我进入了某种无限循环,因为 Mongoose 更新实现显然不希望将对象作为第二个参数。
  4. 如果我也这样做,但在第二个参数中我传递了请求属性的关联数组{status: request.status, phone: request.phone ...} 它可以工作 - 但是我没有参考具体的联系人,也找不到它的createdAt 和@987654329 @ 属性。

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

感谢您的宝贵时间。

【问题讨论】:

标签: javascript mongodb node.js mongoose


【解决方案1】:

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 这个默认没有设置,是自定义属性
【解决方案2】:

我刚刚花了 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 生成它还是被查询的那个?
【解决方案3】:

你很亲密

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 只设置某些字段
【解决方案4】:

好吧,我等了足够长的时间,但没有答案。最后放弃了整个更新/更新方法并选择了:

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: trueupdate docs(但是,更新验证器仅在 $set$unset 操作上运行)
  • 如果您需要.upsert() 在所有型号上都可用,请参阅我的答案。 stackoverflow.com/a/50208331/1586406
【解决方案5】:

我是 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的教程。

【讨论】:

  • 这不会跳过模型验证或“预”中间件吗?
【解决方案6】:

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

【讨论】:

  • 为什么没有投票?似乎是一个很好的解决方案并且非常优雅
  • 出色的解决方案,实际上让我重新思考了我如何处理承诺。
  • 更优雅的是将(model) => { return model.save(); }重写为model => model.save(),并将(err) => { res.send(err); }重写为err => res.send(err);)
【解决方案7】:

我创建了一个 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;
}

然后要更新 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 希望我可以为使用 .save() 提供另一个 +1,因为 findOneAndUpate() 使我们无法使用验证器和 pre、post 等东西.谢谢,我也去看看
  • 抱歉,这里没有用 :( 我的调用堆栈大小超出了
  • 您使用的是哪个版本的 lodash?我正在使用 lodash 版本 2.4.1 谢谢!
  • 另外,你更新插入的对象有多复杂?如果它们太大,节点进程可能无法处理合并对象所需的递归调用次数。
  • 我使用了这个,但必须在保护条件上添加if(_.isObject(value) && _.keys(value).length !== 0) { 以阻止堆栈溢出。 Lodash 4+ 在这里,它似乎将非对象值转换为 keys 调用中的对象,因此递归保护总是正确的。也许有更好的方法,但它现在几乎对我有用......
【解决方案8】:

我需要将一个文档更新/插入到一个集合中,我所做的是创建一个新的对象字面量,如下所示:

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

【讨论】:

    【解决方案9】:
    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 的代码很干净——但您正在获取文档然后进行更新。在该过程的中间,文档可能会被更改。
    【解决方案10】:

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

    【讨论】:

      【解决方案11】:

      您可以简单地用这个更新记录并获取更新的数据作为响应

      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 更新的任何想法。
      【解决方案12】:

      这对我有用。

      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 吗?
      【解决方案13】:

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

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

      【讨论】:

        【解决方案14】:

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

        【讨论】:

          【解决方案15】:

          如果有生成器,它会变得更加容易:

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

          【讨论】:

            【解决方案16】:

            没有其他解决方案适合我。我正在使用发布请求并更新数据,如果发现其他插入它,_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');
            
            });
            

            【讨论】:

              【解决方案17】:

              根据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 }) 之类的操作。

              【讨论】:

                【解决方案18】:
                //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); 
                  });
                });
                
                
                --
                

                【讨论】:

                  【解决方案19】:
                  User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
                      if(err) return res.json(err);
                  
                      res.json({ success: true });
                  });
                  

                  【讨论】:

                  【解决方案20】:

                  一段时间后我才回到这个问题,并决定根据 Aaron Mast 的回答发布一个插件。

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

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

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

                  【讨论】:

                    【解决方案21】:

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

                    【讨论】:

                      【解决方案22】:

                      以 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()
                      【解决方案23】:

                      看完上面的帖子,我决定用这个代码:

                          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 是吗?
                      猜你喜欢
                      • 2018-09-04
                      • 2018-01-13
                      • 2019-07-09
                      • 2015-10-20
                      • 2014-06-12
                      • 2023-03-10
                      • 1970-01-01
                      相关资源
                      最近更新 更多