如何在Mongoose/Node.js中同时保存多个文档?

101

目前,我使用save来添加单个文档。假设我有一个文档数组,我希望将它们存储为单个对象。是否有一种方法可以通过单个函数调用添加它们所有,并在完成后获取单个回调?我可以逐个添加所有文档,但管理回调以确定何时完成所有操作将会很麻烦。


你需要控制代码流程,使用一些像async这样的异步库。(其中有parallel函数,当完成时会调用回调函数) - Risto Novik
https://groups.google.com/forum/#!topic/mongoose-orm/IkPmvcd0kds - arcseldon
13个回答

103

Mongoose 现在支持向 Model.create 方法传递多个文档结构。引用其 API 示例,它可以传递一个数组或一组带有回调函数的对象:

Candy.create({ type: 'jelly bean' }, { type: 'snickers' }, function (err, jellybean, snickers) {
    if (err) // ...
});

或者

var array = [{ type: 'jelly bean' }, { type: 'snickers' }];
Candy.create(array, function (err, jellybean, snickers) {
    if (err) // ...
});

编辑:正如许多人所指出的那样,这并不是执行真正的批量插入 - 它只是隐藏了自己多次调用 save 的复杂性。 为了提高性能,下面的答案和评论解释了如何使用实际的Mongo驱动程序来实现批量插入。


20
注意:这不是批量插入——底层的mongoose实现会逐个遍历所有元素并逐个提交它们。 - outside2344
1
这非常相关,因为它可能会严重影响那些密集使用它的人的性能。 - Lino Silva
1
回复Aaron Heckman 2011:不是很好。Model.create(doc1 [,docN],callback)在某种程度上有所帮助,但仍然会为每个文档调用model.save。如果您的“更快”意味着“绕过所有mongoose钩子和验证”,那么您可以降级到本机驱动程序并直接使用它:Movie.collection.insert(docs,options,callback)https://github.com/christkv/node-mongodb-native/blob/master/lib/mongodb/collection.js#L96-113 - arcseldon
2
我想强调,如果你要处理大量文档,这不是进行批量插入的最佳方式。请参见https://dev59.com/n2Qn5IYBdhLWcg3wkH7U#24848148,其中包含更好的解释。 - Lucio Paiva
非常感谢朋友...真的很有帮助 - Akarsh Satija
显示剩余2条评论

85

Mongoose 4.4增加了一个方法叫做insertMany

验证文档数组并在它们全部有效时将它们插入到MongoDB的快捷方式。此函数比.create()更快,因为它只向服务器发送一次操作,而不是每个文档都发送一次。

引用自#723的vkarpov15:

折衷方案是insertMany()不会触发pre-save钩子,但它应该具有更好的性能,因为它只向数据库进行单次往返,而不是每个文档都进行一次。

该方法的签名与create完全相同:

Model.insertMany([ ... ], (err, docs) => {
  ...
})

或者,使用 promises:

Model.insertMany([ ... ]).then((docs) => {
  ...
}).catch((err) => {
  ...
})

谢谢你。这里写的是,如果它们全部有效,它将插入它们;这是否意味着如果一个失败了,所有都会失败? - Aron
1
这是一个批量操作,但不是原子操作。我不确定Mongoose如何处理它,现在无法测试,但它应该返回成功写入的数量。MongoDB文档中有更多细节:https://docs.mongodb.com/manual/reference/method/db.collection.insertMany/#error-handling - Pier-Luc Gendreau
10
如果 insertMany 失败,则不会插入任何内容,我已经进行了测试。 - shontauro
在需要验证重复文档插入的情况下,插入许多文档可能会很麻烦。如果新文档中已经指定了 _id,它似乎可以正常工作;但对于那些没有指定 _id 的文档,它会抛出重复错误。 - zinoadidi
1
@shontauro 现在可以进行配置,在使用Model.insertMany(docs [, options])时,将布尔值“false”传递给“options”参数,这样它就不会快速失败。 - aderchox

43

Mongoose目前还没有实现批量插入功能(请参见问题#723)。

既然您知道要保存的文档数量,您可以编写以下代码:

var total = docArray.length
  , result = []
;

function saveAll(){
  var doc = docArray.pop();

  doc.save(function(err, saved){
    if (err) throw err;//handle error

    result.push(saved[0]);

    if (--total) saveAll();
    else // all saved here
  })
}

saveAll();

当然,这只是一个权宜之计,我建议使用某种类型的流程控制库(我使用q,非常棒)。


2
你能用Q语言提供解决方案吗? - Manu
9
我不认为这是“并发”的。每次保存操作只有在上一次完成后才会被调用。 - Ted Bigham
真的。更加并发的方法是,例如,触发所有的“保存”,等待所有的回调调用并返回结果数组。你可以使用async来实现,或者一些promise接口。 - diversario
这个条件if (--total)何时会为假? - Gobliins
3
上面的答案过时了,mongoose中有一个名为insertMany()的方法。请查看https://mongoosejs.com/docs/api.html#model_Model.insertMany - Epsi95

26

1
回复Aaron Heckman 2011:不是很好。Model.create(doc1 [,docN],callback)在某种程度上有所帮助,但仍然会为每个文档调用model.save。如果您的“更快”意味着“绕过所有mongoose钩子和验证”,那么您可以降级到本机驱动程序并直接使用它:Movie.collection.insert(docs,options,callback)https://github.com/christkv/node-mongodb-native/blob/master/lib/mongodb/collection.js#L96-113 - arcseldon
1
我一直看到这个答案,但这并不是真正的“mongoose”方式。这将完全绕过Mongoose模型。如果您在mongoose模型中设置了某些字段的默认值,则它们将被忽略,并且不会插入到数据库中。 - Nahn
1
如何在Mongoose中使用Model.collection.insert?请提供一个例子。 - Stephan Kristyn
1
我知道有些人对这种方法持批评态度,但如果你要处理大量文档,这实际上是最好的(如果不是唯一的)答案。这个其他答案(https://dev59.com/n2Qn5IYBdhLWcg3wkH7U#24848148)解释了为什么它更好,并给出了一个例子。 - Lucio Paiva
有人可以建议一下可能的选项吗? - Noushad

17

使用 async.parallel,您的代码将如下所示:

  async.parallel([obj1.save, obj2.save, obj3.save], callback);

由于在Mongoose中和async中的约定是相同的,即(err, callback),因此您不需要将它们包装在自己的回调函数中,只需将保存调用添加到数组中,等待所有完成时您将获得回调。

如果您使用mapLimit,您可以控制要并行保存多少个文档。在此示例中,我们并行保存10个文档,直到所有项目都成功保存。

async.mapLimit(myArray, 10, function(document, next){
  document.save(next);
}, done);

2
有趣 - 你介意给出一个实际可用的例子,其中 myArray 包含1000万个项目。 - Stephan Kristyn

8
我知道这是一个老问题,但我担心这里没有正确的答案。大多数答案只是谈论遍历所有文档并单独保存每个文档,如果你有很多文档,这是一个坏主意,而且即使在许多请求中重复进行这个过程也会导致效率低下。
具体来说,MongoDB有一个batchInsert()调用可以插入多个文档,应该从原生mongodb驱动程序中使用这个功能。Mongoose是基于此驱动程序构建的,并不支持批量插入。这可能是有道理的,因为它被认为是MongoDB的对象文档建模工具。
解决方案: Mongoose附带了原生的MongoDB驱动程序。您可以通过require('mongoose/node_modules/mongodb')来使用该驱动程序(这个方法可能不太确定,但如果出现问题,您可以重新安装mongodb npm包)。然后就可以进行适当的batchInsert了。

2
错误,Pascal的答案完全没有抓住重点。需要大量插入的人往往是因为他们想一次性插入10,000,000个项目。如果没有批量插入,本应只需几秒钟的操作可能需要花费几个小时。Model.create是一个史诗级失败,因为它假装是一个批量插入,但在底层它只是一个for循环。 - user3690202
Mongoose 真的需要进行一些改进。此外,他们的文档还有很多需要完善的地方。 - Stephan Kristyn
我认为@Yashua的问题通过使用底层的mongodb JavaScript驱动程序来解决。 - Ehtesh Choudhury

8

较新版本的MongoDB支持批量操作:

var col = db.collection('people');
var batch = col.initializeUnorderedBulkOp();

batch.insert({name: "John"});
batch.insert({name: "Jane"});
batch.insert({name: "Jason"});
batch.insert({name: "Joanne"});

batch.execute(function(err, result) {
    if (err) console.error(err);
    console.log('Inserted ' + result.nInserted + ' row(s).');
}

8
使用insertMany函数插入多个文档。这只向服务器发送一个操作,并在命中mongo服务器之前使用Mongoose验证所有文档。默认情况下,Mongoose按照数组中存在的顺序插入项目。如果您不想保持任何顺序,请设置ordered:false
重要提示 - 错误处理:
ordered:true时,验证和错误处理会成组发生,也就是说如果有一个失败,所有操作都将失败。
ordered:false时,验证和错误处理将单独进行,并且操作将继续进行。错误将以错误数组的形式返回。

5

以下是另一种方法,不需要使用额外的库(未包括错误检查)

function saveAll( callback ){
  var count = 0;
  docs.forEach(function(doc){
      doc.save(function(err){
          count++;
          if( count == docs.length ){
             callback();
          }
      });
  });
}

3

您可以使用由mongoose返回的Promise save,mongoose中的Promise并不具有所有特性,但是您可以使用此模块添加功能。

创建一个模块,增强mongoose承诺的所有功能。

var Promise = require("mongoose").Promise;

Promise.all = function(promises) {
  var mainPromise = new Promise();
  if (promises.length == 0) {
    mainPromise.resolve(null, promises);
  }

  var pending = 0;
  promises.forEach(function(p, i) {
    pending++;
    p.then(function(val) {
      promises[i] = val;
      if (--pending === 0) {
        mainPromise.resolve(null, promises);
      }
    }, function(err) {
      mainPromise.reject(err);
    });
  });

  return mainPromise;
}

module.exports = Promise;

然后使用mongoose:
var Promise = require('./promise')

...

var tasks = [];

for (var i=0; i < docs.length; i++) {
  tasks.push(docs[i].save());
}

Promise.all(tasks)
  .then(function(results) {
    console.log(results);
  }, function (err) {
    console.log(err);
  })

在 Promise.js 中,未捕获的 TypeError:Promise 解析程序未定义为函数。 - krishan kumar

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