如何解决MongoDB/Node异步问题?

6

I have the following code:

// Retrieve
var MongoClient = require("mongodb").MongoClient;
var accounts = null;
var characters = null;

// Connect to the db
MongoClient.connect("mongodb://localhost:27017/bq", function(err, db) {
   if(err) { return console.dir(err); }

    db.createCollection('accounts', function(err, collection) {
        if(err) { return console.dir(err); }
        else { accounts = collection; }

        createAccount("bob","bob");
        createAccount("bob","bob");
        createAccount("bob","bob");
        createAccount("bob","bob");
    });
});


function createAccount(email, password)
{
    accounts.findOne({"email":email}, function(err, item) {
        if(err) { console.dir(err); }
        else {
            if(item === null) {
                accounts.insert({"email":email, "password":password}, function(err, result) {
                    if(err) { console.dir(err); }
                    else { console.dir("Account " + email + " created."); }
                });
            }
            else {
                console.dir("Account already exists.")
            }

        }
    });
}

当我第一次运行脚本时,bob会有4个账户。第二次运行时,我会收到4条消息,提示该账户已存在。
我相信我知道这是为什么,我想到的解决办法是在处理每个数据库读/写操作时使用某种队列,以便逐个进行。我想知道的是,这是否是正确的做法,以及这方面的一般最佳实践是什么?

1
你想让第二、第三和第四个插入操作失败吗? - Fabrício Matté
是的,因为该账户应该已经存在(但目前还不存在)。 - user2037584
3
最佳做法是在“email”上添加唯一索引,如果出现重复,则将“insert”错误处理为另一种“帐户已存在”错误的变体。 - JohnnyHK
那听起来是一个不错的处理方式。 - user2037584
你也可以尝试使用Promise来解决问题,参见:http://stackoverflow.com/questions/11912573/node-js-deferred-promisify-mongoose/11931908#11931908 - Mariusz Nowak
3个回答

10

一些语言提供了特殊的语言结构来解决这个问题。例如,C#有async/await关键字,让您编写代码就像调用同步API一样。

JavaScript没有,您必须使用回调函数链接createAccount调用。

一些人开发了库来帮助您组织此类代码。例如asyncstepnode-promiseQ

您还可以使用fibers库,这是一个原生库,通过纤程/协程扩展了JavaScript运行时。

一些人已经使用类似于async/await的结构扩展了语言:streamline.jsIcedCoffeeScriptwind.js。例如,streamline.js(我是作者,所以显然有偏见)使用_作为特殊的回调占位符,让您可以将示例编写为:

var db = MongoClient.connect("mongodb://localhost:27017/bq", _):
var accounts = db.createCollection('accounts', _);
createAccount("bob","bob", _);
createAccount("bob","bob", _);
createAccount("bob","bob", _);
createAccount("bob","bob", _);

function createAccount(email, password, _) {
    var item = accounts.findOne({"email":email}, _);
    if (item === null) {
        accounts.insert({"email":email, "password":password}, _);
        console.log("Account " + email + " created."); }
    } else {
        console.log("Account already exists.")
    }
}

最后但并非最不重要的,新的语言特性例如生成器延迟函数正在讨论用于未来版本的JavaScript(生成器很可能会出现在ES6中,延迟函数似乎有些停滞)。

所以您有多种选择:

  • 坚持使用回调函数
  • 使用帮助程序库
  • 使用纤程运行时扩展
  • 使用语言扩展
  • 等待ES6的发布

值得一提的另一个选项是tamejs,它与IcedCoffeeScript的开发者相同 - 实际上,它是在普通JS中工作的原始版本。但由于某种原因,它生成的代码量是streamline.js的两倍(尽管函数数量大致相同),因此我建议使用streamline.js。此外,streamline.js允许您使用try/catch更自然地处理错误,并具有使其更快的fibers选项。 - Matt Browne
值得注意的是,streamline.js 也可以与 CoffeeScript 一起使用(通过在 CoffeeScript 转换之后应用 Streamline.js 转换;有关详细信息,请参阅文档)。 - Matt Browne

0
在电子邮件上添加唯一约束,您就不必再检查用户是否存在了!

-1

JavaScript是异步的。accounts.findOne立即返回,因此基本上您的4个语句都会同时执行。

accounts.findOne的作用是查找一个{"email":email},并在找到它时运行第二个参数中的函数。然后它返回该函数并继续下一个CreateAccount语句。与此同时,当结果从硬盘返回(这比执行这些语句要花费更长时间)时,它进入函数,由于没有用户,因此添加一个用户。有道理吗?

更新 这是在JavaScript中正确的做法。

MongoClient.connect("mongodb://localhost:27017/bq", function(err, db) {
   if(err) { return console.dir(err); }

    db.createCollection('accounts', function(err, collection) {
        if(err) { return console.dir(err); }
        else { accounts = collection; }

        createAccount("bob","bob", function() {
            createAccount("bob","bob", function() {
                createAccount("bob","bob", function() {
                    createAccount("bob","bob", function() {
                     });
                });
            });
        });
    });
});


function createAccount(email, password, fn)
{
    accounts.findOne({"email":email}, function(err, item) {
        if(err) { console.dir(err); }
        else {
            if(item === null) {
                accounts.insert({"email":email, "password":password}, function(err, result) {
                    if(err) { console.dir(err); }
                    else { console.dir("Account " + email + " created."); }
                    fn();
                });
            }
            else {
                console.dir("Account already exists.")
                fn();
            }

        }
    });
}

我明白为什么会发生这种情况,我想知道的是如何最好地解决它。 - user2037584
我在上面添加了代码,展示了在JavaScript中正确的做法。或者,您可以使用Step库https://github.com/creationix/step。 - Tarandeep Gill
4
基于这篇回答更新中的内容:“这是在JavaScript中正确的做法”,我倾向于给它一个负面评价。首先,这篇回答讨论的是Node.js中异步使用的问题,而不是JavaScript,代码中很少有一种“正确”的方式。希望任何理性的开发者都能认识到,在这种情况下,嵌套的回调函数不具备可扩展性,并不是最理想的解决方案。我同意以下引用:“然而,超过几个层次的嵌套就应该成为一个代码气味-是时候考虑将什么抽象出来成为单独、小的模块了。” via http://book.mixu.net/node/ch7.html - Mark Edington

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