【问题标题】:Create and Insert documents using an async while loop使用异步 while 循环创建和插入文档
【发布时间】:2017-12-25 11:20:20
【问题描述】:

我正在尝试动态生成令牌并将它们保存到数据库中。

这是生成令牌的代码。

const generateToken = function (maxUse) {
  // 12 digit token numbers. 9e+11 possibilities
  const min = 100000000000;
  const max = 999999999999;
  const token =  Math.floor(Math.random() * (max -min) + min);
  // ensure token doesn't exit exist in db before saving
  Token.count({ token }, function (err, count) {
    if (count > 0) {
      generateToken() ;
    } else {
      let newToken = new Token({ token, maxUse });
      newToken.save(function (err, savedToken) {
        if (err) {
          console.log(err);
          return;
        } else {
          generateSerial(savedToken._id);
          console.log("saved token is =>", savedToken.token);
          return savedToken.token;
        }
      })
    }
  })
}

我如何编写一个调用此函数任意次数的函数,在将令牌保存到数据库时将它们附加到文件中。我意识到,由于进程的异步性质,while 循环不会起作用。

我看到的所有答案都假设我提前拥有大量数据,例如使用 bulkwrite (mongoose)。

欢迎使用替代方法

谢谢。

【问题讨论】:

    标签: javascript node.js mongodb mongoose promise


    【解决方案1】:

    在我看来,您最好保留生成令牌的“本地列表”并通过.insertMany() 进行“批量”插入。实际实现的里程可能会有所不同,因此我们将讨论该方法以及使用异步方法以理智的方式处理您的递归函数。

    异步循环

    您创建了一个问题,您需要测试存在的值以确定它们对于插入是“唯一的”。这当然需要异步调用才能查看数据库,因此排除了诸如“upserts”之类的“批量”操作,因为在循环发送之前您不知道该项目是否存在。所以递归在这种情况下确实有效。

    所以你应该做的第一件事就是让“函数”本身异步,要么返回一个回调,要么返回一个承诺。

    本质上:

    function generateToken(maxUse) {
      const min = 100000000000;
      const max = 999999999999;
      const token =  Math.floor(Math.random() * (max -min) + min);
    
      return Token.count({ token }).then( count => {
    
        if ( count > 0 ) {
          generateToken(maxUse);
        } else {
          return Token.create({ token, maxUse });
        }
    
      })
    }
    

    或者用更现代的术语使用 async/await

    async function generateToken(maxUse) {
      const min = 100000000000;
      const max = 999999999999;
      const token =  Math.floor(Math.random() * (max -min) + min);
    
      let count = await Token.count({ token });
    
      if ( count > 0 ) {
        generateToken(maxUse);
      } else {
          return Token.create({ token, maxUse });
      }
    }
    

    那么这实际上只是一个循环调用的问题,或者用现代术语来说:

    let count = 0;
    while (count < 500) {
      // Random usage 1-5
      const maxUse = Math.floor(Math.random() * 5) + 1;
      let token = await generateToken(maxUse);
      log(token.token);
      count++;
    }
    

    如果在不支持async/await的节点版本下运行,则使用async.whilst

     asyncWhilst(
        () => count < 500,
        (callback) => {
          const maxUse = Math.floor(Math.random() * 5 ) + 1;
          generateToken(maxUse).then(token => {
            log(token.token);
            count++;
            callback();
          }).catch(err => callback(err));
        },
        (err) => {
          if (err) throw err;
          // Loop complete, issue callback or promise
        }
      );
    

    所以一切都比较简单。


    保持唯一的本地和“批量插入”

    处理此问题的“替代”方法是将生成的令牌数组保留在“客户端”上。然后,您在每次随机生成时所需要做的就是查看令牌是否“已经看到”,并且仅在获得“唯一”值时才创建插入操作。

    这应该比通过递归调用来回访问数据库要快得多,因为它都是在本地“缓存”的。

    本质上,让你的生成器功能非常基本:

    function generateToken(maxUse) {
      const min = 100000000000;
      const max = 999999999999;
      const token =  Math.floor(Math.random() * (max -min) + min);
    
      return ({ token, maxUse });
    }
    

    然后在循环期间,为seenTokensops 创建两个数组,其中后者表示稍后以“批量”而不是单独写入的方式插入的项目:

    let count = 0,
        seenTokens = [],
        ops = [];
    
    while ( count < 500 ) {
      const maxUse = Math.floor(Math.random() * 5) + 1;
    
      let token = generateToken(maxUse);
    
      if ( seenTokens.indexOf(token.token) === -1 ) {
        seenTokens.push(token.token);
        ops.push(token);
        count++
    
        if ( count % 500 === 0 ) {
          await Token.insertMany(ops);
          ops = [];
        }
      } else {
        continue
      }
    
    }
    
    if ( count % 500 !== 0 ) {
      await Token.insertMany(ops);
      ops = [];
    }
    

    当然,我们在那里应用 async/await 方法,但这只是因为 .insertMany() 方法是异步的,如果您实际上没有插入“数万”,那么它应该很容易处理,甚至不需要“ await" 这样的调用,然后只发出 "once"

    但是这里的演示说明了当代码“数以万计”没有其他更改时应该是什么样子。同样,您可以根据需要使用其他库函数来“等待”此类调用。

    我们可以再次使用async.seriesasync.whilst 进行此类控制:

      let count = 0,
        seenTokens = [],
        ops = [];
    
      asyncSeries(
        [
          (callback) =>
            asyncWhilst(
              () => count < 500,
              (callback) => {
                const maxUse = Math.floor(Math.random() * 5) + 1;
    
                let token = generateToken(maxUse);
    
                if ( seenTokens.indexOf(token.token) === -1 ) {
                  seenTokens.push(token.token);
                  ops.push(token);
                  count++;
    
                  if ( count % 500 === 0 ) {
                    Token.insertMany(ops,(err,response) => {
                      console.log(count);
                      ops = [];
                      callback(err);
                    });
                  } else {
                    callback();
                  }
                } else {
                  console.log("trying again: seen token %s", token.token);
                  callback();
                }
              },
              callback
            ),
    
          (callback) => {
            if ( count % 500 !== 0 ) {
              Token.insertMany(ops,callback)
            } else {
              callback()
            }
    
          }
        ],
        (err) => {
          if (err) throw err;
          ops = [];
          // Operations complete, so callback to continue
        }
      );
    

    一切都一样,“流控制”实际上只是为了迎合“大批量”,您可以简单地使用常规循环来构建ops 条目并只调用@ 987654326@,就像这里的500 限制实际上一样。

    所以最简单的形式基本上是:

    let count = 0,
        seenTokens = [],
        ops = [];
    
    // Regular loop
    while ( count < 500 ) {
      const maxUse = Math.floor(Math.random() * 5) + 1;
    
      let token = generateToken(maxUse);
    
      if ( seenTokens.indexOf(token.token) === -1 ) {
        seenTokens.push(token.token);
        ops.push(token);
        count++;
      }
    }
    // Insert all at once
    Token.insertMany(ops,(err,result) => {
      if (err) throw err;
      // now it's complete
    })
    

    当然,这整个替代方法“取决于”这样一个事实,即您从未真正维护数据库中“令牌”的“持久性”,并且在清除那些现有条目之前不会再次调用此函数。我们可以在所有“获取的令牌”中“啜饮”并通过相同的“本地缓存”排除。但随着时间的推移,这将显着增长,因此在您的整体选择中需要考虑这一点。


    作为最新 nodejs 版本的完整列表,但内部应用了一般用法:

    const asyncWhilst = require('async').whilst,
          mongoose = require('mongoose'),
          Schema = mongoose.Schema;
    
    mongoose.Promise = global.Promise;
    mongoose.set('debug', true);
    
    const uri = 'mongodb://localhost/test',
          options = { useMongoClient: true };
    
    const tokenSchema = new Schema({
      token: { type: Number, unique: true },
      maxUse: Number
    });
    
    const Token = mongoose.model('Token', tokenSchema);
    
    // Logger helper
    
    function log(data) {
      console.log(JSON.stringify(data,undefined,2))
    }
    
    
    // Function implementation
    function generateToken(maxUse) {
      const min = 100000000000;
      const max = 999999999999;
      const token =  Math.floor(Math.random() * (max -min) + min);
    
      return Token.count({ token }).then( count => {
    
        if ( count > 0 ) {
          generateToken(maxUse);
        } else {
          return Token.create({ token, maxUse });
        }
    
      })
    }
    
    // Main program
    (async function() {
    
      try {
        const conn = await mongoose.connect(uri,options);
    
        console.log("using async/await");
        // clean data
        await Promise.all(
          Object.keys(conn.models).map(m => conn.models[m].remove({}))
        );
    
        let count = 0;
        while (count < 500) {
          // Random usage 1-5
          const maxUse = Math.floor(Math.random() * 5) + 1;
          let token = await generateToken(maxUse);
          log(token.token);
          count++;
        }
    
        let totalCount = await Token.count();
        console.log("Count is: %s", totalCount);
    
        // Or using async.whilst
        console.log("Using async.whilst");
        // clean data
        await Promise.all(
          Object.keys(conn.models).map(m => conn.models[m].remove({}))
        );
    
        count = 0;
        await new Promise((resolve,reject) => {
          asyncWhilst(
            () => count < 500,
            (callback) => {
              const maxUse = Math.floor(Math.random() * 5 ) + 1;
              generateToken(maxUse).then(token => {
                log(token.token);
                count++;
                callback();
              }).catch(err => callback(err));
            },
            (err) => {
              if (err) reject(err);
              resolve();
            }
          );
        });
    
        totalCount = await Token.count();
        console.log("Count is: %s", totalCount);
    
      } catch (e) {
        console.error(e);
      } finally {
        mongoose.disconnect();
      }
    
    })();
    

    或作为“替代”过程:

    const asyncSeries = require('async').series,
          asyncWhilst = require('async').whilst,
          mongoose = require('mongoose'),
          Schema = mongoose.Schema;
    
    mongoose.Promise = global.Promise;
    mongoose.set('debug', true);
    
    const uri = 'mongodb://localhost/test',
          options = { useMongoClient: true };
    
    const tokenSchema = new Schema({
      token: { type: Number, unique: true },
      maxUse: Number
    });
    
    const Token = mongoose.model('Token', tokenSchema);
    
    // Logger helper
    
    function log(data) {
      console.log(JSON.stringify(data,undefined,2))
    }
    
    
    // Function implementation
    function generateToken(maxUse) {
      const min = 100000000000;
      const max = 999999999999;
      const token =  Math.floor(Math.random() * (max -min) + min);
    
      return ({ token, maxUse });
    }
    
    // Main program
    (async function() {
    
      try {
        const conn = await mongoose.connect(uri,options);
    
        console.log("Using async/await");
        // clean data
        await Promise.all(
          Object.keys(conn.models).map(m => conn.models[m].remove({}))
        );
    
        let count = 0,
            seenTokens = [],
            ops = [];
    
        while ( count < 500 ) {
          const maxUse = Math.floor(Math.random() * 5) + 1;
    
          let token = generateToken(maxUse);
    
          if ( seenTokens.indexOf(token.token) === -1 ) {
            seenTokens.push(token.token);
            ops.push(token);
            count++
    
            if ( count % 500 === 0 ) {
              await Token.insertMany(ops);
              ops = [];
            }
          } else {
            continue
          }
    
        }
    
        if ( count % 500 !== 0 ) {
          await Token.insertMany(ops);
          ops = [];
        }
    
        totalCount = await Token.count();
        console.log("Count is: %s", totalCount);
    
        // using async.whilst and indeed async.series for control
        console.log("using asyc.whilst");
        await Promise.all(
          Object.keys(conn.models).map(m => conn.models[m].remove({}))
        );
    
        await new Promise((resolve,reject) => {
          count = 0,
          seenTokens = [],
          ops = [];
    
          asyncSeries(
            [
              (callback) =>
                asyncWhilst(
                  () => count < 500,
                  (callback) => {
                    const maxUse = Math.floor(Math.random() * 5) + 1;
    
                    let token = generateToken(maxUse);
    
                    if ( seenTokens.indexOf(token.token) === -1 ) {
                      seenTokens.push(token.token);
                      ops.push(token);
                      count++;
    
                      if ( count % 500 === 0 ) {
                        Token.insertMany(ops,(err,response) => {
                          console.log(count);
                          ops = [];
                          callback(err);
                        });
                      } else {
                        callback();
                      }
                    } else {
                      console.log("trying again: seen token %s", token.token);
                      callback();
                    }
                  },
                  callback
                ),
    
              (callback) => {
                if ( count % 500 !== 0 ) {
                  Token.insertMany(ops,callback)
                } else {
                  callback()
                }
    
              }
            ],
            (err) => {
              if (err) reject(err);
              ops = [];
              resolve();
            }
          );
    
        });
    
        totalCount = await Token.count();
        console.log("Count is: %s", totalCount);
    
    
      } catch (e) {
        console.error(e);
      } finally {
        mongoose.disconnect();
      }
    
    })();
    

    【讨论】:

    • 非常感谢。我需要一分钟来吸收你写的所有东西。我不知道清理与模型相关的数据。你能解释一下吗?谢谢
    • @segunchrist 这只是一个演示。所以我所做的就是在创建“新”数据之前从集合中删除任何内容。每种情况下的结果是创建了 500 个“唯一”令牌。这就是完整列表的意义所在。因此,您可以自己运行它并查看它是否正常工作。然后“之后”你坐下来弄清楚“它为什么起作用”。但这也在答案本身中得到了解释。主要的是“你的函数本身需要是异步的”,以及处理它的“循环”。
    • 好的。非常感谢
    • @segunchrist 没关系。添加了另一个示例,说明如何通过“本地”持有“已经生成的”令牌来实现这一点。因此,不需要异步调用来“查找”现有令牌,您可以简单地查看“本地数组”并继续循环直到一个唯一的。这将是一种比重复和递归异步调用更有效的方法。
    • 非常感谢您提供的示例代码。确保批次中的每个新令牌从未存在于数据库中至关重要,因为当前批次的令牌将立即打印给用户,因此在内存中作为数组缓冲可能会导致数据库中丢失一些打印的令牌
    猜你喜欢
    • 1970-01-01
    • 2019-10-08
    • 1970-01-01
    • 2016-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-07
    • 2015-01-14
    相关资源
    最近更新 更多