(mongoose/promises) 如何检查使用findOneAndUpdate与upsert创建的文档

5
考虑以下代码,我需要创建或更新特定的文档。
Inbox.model.findOneAndUpdate({ number: req.phone.number }, {
    number: req.phone.number,
    country: req.phone.country,
    token: hat(),
    appInstalled: true
}, { new: true, upsert: true }).then(function(inbox){
    /*
       do something here with inbox, but only if the inbox was created (not updated)
    */
});

mongoose是否有区分文档是创建还是更新的功能?我需要使用new: true,因为我需要在inbox上调用函数。
2个回答

6
在mongoose中,.findOneAndUpdate()或任何.findAndModify()核心驱动程序变量的情况下,实际回调签名有“三个”参数:
 function(err,result,raw)

第一个参数是错误响应,第二个参数是根据选项修改或原始文档,第三个参数是所发出语句的写入结果。

第三个参数应该返回类似于这样的数据:

{ lastErrorObject:
   { updatedExisting: false,
     n: 1,
     upserted: 55e12c65f6044f57c8e09a46 },
  value: { _id: 55e12c65f6044f57c8e09a46, 
           number: 55555555, 
           country: 'US', 
           token: "XXX", 
           appInstalled: true,
           __v: 0 },
  ok: 1 }

在这里,lastErrorObject.updatedExisting 的一致字段为 true/false 取决于是否发生了 upsert。请注意,当此属性为 false 时,还有一个包含新文档的 _id 响应的 "upserted" 值,但是当它为 true 时,没有该值。

因此,您需要修改处理方式以考虑第三种情况,但这仅适用于回调而不是 promise:

Inbox.model.findOneAndUpdate(
    { "number": req.phone.number },
    { 
      "$set": {
          "country": req.phone.country,
          "token": hat(),
          "appInstalled": true
      }
    }, 
    { "new": true, "upsert": true },
    function(err,doc,raw) {

      if ( !raw.lastErrorObject.updatedExitsing ) {
         // do things with the new document created
      }
    }
);

我强烈建议您在此处使用更新操作符而不是原始对象,因为原始对象将始终覆盖整个文档,而$set等操作符只影响列出的字段。
还要注意,只要它们的值是未找到的精确匹配,任何匹配的“查询参数”都会自动分配给新文档。
考虑到使用Promise似乎无法出于某种原因返回附加信息,那么除了设置{new:false}并且基本上没有返回文档时就是新文档之外,我不认为这是Promise可以实现的。
您已经拥有预期插入的所有文档数据,因此您实际上并不需要返回该数据。事实上,这正是本机驱动程序方法在核心处理此问题的方式,并且仅在发生插入时才响应“upserted”_id值。
这实际上归结为本站讨论的另一个问题,详见:

承诺对象的onFulfilled函数可以有多个参数吗?

这实际上涉及到在一个承诺响应中解析多个对象的问题,这是原生规范不直接支持的,但有一些方法在那里列出。

所以如果您实现了Bluebird承诺并使用.spread()方法,则一切都很好:

var async = require('async'),
    Promise = require('bluebird'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var testSchema = new Schema({
  name: String
});

var Test = mongoose.model('Test',testSchema,'test');
Promise.promisifyAll(Test);
Promise.promisifyAll(Test.prototype);

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      var promise = Test.findOneAndUpdateAsync(
        { "name": "Bill" },
        { "$set": { "name": "Bill" } },
        { "new": true, "upsert": true }
      );

      promise.spread(function(doc,raw) {
        console.log(doc);
        console.log(raw);
        if ( !raw.lastErrorObject.updatedExisting ) {
          console.log( "new document" );
        }
        callback();
      });
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

当然,它会返回两个对象,您可以一致地访问它们:
{ _id: 55e14b7af6044f57c8e09a4e, name: 'Bill', __v: 0 }
{ lastErrorObject:
   { updatedExisting: false,
     n: 1,
     upserted: 55e14b7af6044f57c8e09a4e },
  value: { _id: 55e14b7af6044f57c8e09a4e, name: 'Bill', __v: 0 },
  ok: 1 }

这是一个完整的列表,展示了正常的行为:
var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var testSchema = new Schema({
  name: String
});

var Test = mongoose.model('Test',testSchema,'test');

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      Test.findOneAndUpdate(
        { "name": "Bill" },
        { "$set": { "name": "Bill" } },
        { "new": true, "upsert": true }
      ).then(function(doc,raw) {
        console.log(doc);
        console.log(raw);
        if ( !raw.lastErrorObject.updatedExisting ) {
          console.log( "new document" );
        }
        callback();
      });
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

记录一下,本地驱动程序本身并没有这个问题,因为响应对象实际上是它唯一返回的对象,除了任何错误。
var async = require('async'),
    mongodb = require('mongodb'),
    MongoClient = mongodb.MongoClient;

MongoClient.connect('mongodb://localhost/test',function(err,db) {

  var collection = db.collection('test');

  collection.findOneAndUpdate(
    { "name": "Bill" },
    { "$set": { "name": "Bill" } },
    { "upsert": true, "returnOriginal": false }
  ).then(function(response) {
    console.log(response);
  });
});

所以它总是像这样:

{ lastErrorObject:
   { updatedExisting: false,
     n: 1,
     upserted: 55e13bcbf6044f57c8e09a4b },
  value: { _id: 55e13bcbf6044f57c8e09a4b, name: 'Bill' },
  ok: 1 }

根据此链接 https://github.com/Automattic/mongoose/issues/2931,findAndModify仅提供文档(而非原始数据)。 - Matt Way
有什么想法为什么当我尝试使用回调而不是 Promise 时,“raw” 总是未定义的? - Matt Way
@MattWay 正在努力挖掘,但目前明确的事情似乎只有 promise 只返回文档响应而没有其他参数。如果我找到了具体的东西,我会添加它。 - Blakes Seven
如果我理解正确的话... 如果你只想使用bluebird的.spread()方法,那么promisifyAll()是非常昂贵的。毕竟,.spread()什么都做不了,你可以用.then()来代替它。 - Roamer-1888
@Roamer-1888 重点在于所需的“回调签名”会返回比“错误”响应更有用的“两个”参数。因此,“成功”不是所需响应中的单个对象,其中所需细节来自“第二个”对象响应。这不受.then()支持。因此,您需要以“某种方式”定义它,以便.spread()可用于接受两个响应。理想情况下,这将是ES6,您只需转换返回值即可。因此是不正确的。在这种情况下,.spread()执行一些.then()无法执行的操作。 - Blakes Seven
显示剩余8条评论

1

对于使用Promise的人,根据Mongoosejs.com上的此链接,我们可以将rawResult: true与其他选项一起传递。

const filter = { name: 'Will Riker' };
const update = { age: 29 };

await Character.countDocuments(filter); // 0

let res = await Character.findOneAndUpdate(filter, update, {
  new: true,
  upsert: true,
  rawResult: true // Return the raw result from the MongoDB driver
});

res.value instanceof Character; // true
// The below property will be `false` if MongoDB upserted a new
// document, and `true` if MongoDB updated an existing object.
res.lastErrorObject.updatedExisting; /


这个查询的结果格式将会是:
{ lastErrorObject:
   { n: 1,
     updatedExisting: false,
     upserted: 5e6a9e5ec6e44398ae2ac16a },
  value:
   { _id: 5e6a9e5ec6e44398ae2ac16a,
     name: 'Will Riker',
     __v: 0,
     age: 29 },
  ok: 1 }

因此,该文档可以通过data.value访问。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接