2016年1月6日 星期三

利用 Async.js 平行處理多筆 MongoDB 查詢

在使用 MongoDB 資料庫的時候,我們常常需要同時送出多筆查詢,並在所有資料都讀取完畢後進行下一步動作,例如將資料顯示在網頁上。這篇文章將會簡單介紹利用 Async.jsmongoose 達成上述需求的方法與程式碼。

Copyright jaketrent.com

Async.js 簡介


在 javascript 中雖然有著 callback 機制,可以在事件完成後接著執行其他動作。但如果要等待很多事件完成,可能會寫成很多層的 callback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var findData = function(input, callback){
    var data = new Data;
    findMoreData(inputA, function(dataA){
        data.add(dataA);
        findMoreData(inputB, function(dataB){
            data.add(dataB);
            findMoreData(inputC, function(dataC){
                ...
            });
        });
    });
    callback(data);
}

這種寫法不僅難看,而且由於並非平行處理,在每個 findMoreData 函式中等待 I/O 處理的時間都會累積起來,影響效能。

我們可以使用 Async.js 套件提供的 parallel 方法,來改寫上面的程式碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var async = require('async');
var findData = function(input, callback){
    async.parallel({
        A: function(cb){
            findMoreData(inputA, function(dataA){
                cb(null, dataA);
            });
        },
        B: function(cb){
            findMoreData(inputB, function(dataB){
                cb(null, dataB);
            });
        }, 
        ...
    }, 
    // 這裡是 async.parallel 的 callback function
    function(err, results) {
        // 在這裡 results 的內容是個物件:{A: dataA, B: dataB, ...}
        callback(results);
    });
}

上面的 async.parallel 方法接受一個物件當作參數,裡面包含各個需要平行處理的工作。範例中為了獲得資料 A,我們呼叫了  findMoreData 來處理,而在 findMoreData 等待 I/O 處理的時間中,parallel 會試著繼續處理資料 B 的部分。因此,我們可以平行化的處理這些工作,不用等待前一個完成才接著執行。

程式碼中的 cb 是 Async.js 的一個 callback function。執行後,parallel 就會認為這部分的工作已經完成,而我們要確保每個工作都會呼叫到 cb,整個 parallel 的處理流程才會正常結束。cb 接受兩個參數,第一個是 error 物件(可以為 null),第二個則是我們要的資料,它會存到 results 裡面。

等到獲得所有資料,最後一段的 callback function就會被執行,讓使用者可以處理後續工作。

parallel 與 Mongoose 的整合


對於 Async.js 有簡單的認識後,我們正式把 mongoose 讀取資料的 API 整合進來:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var async = require('async');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var dataASchema = new Schema({...});
var dataAModel = mongoose.model('dataA', dataASchema);
...
var findData = function(callback){
    async.parallel({
        A: function(cb){
            dataAModel.find({}, function(err, data, count){
                cb(null, data);
            });
        },
        B: function(cb){
            dataBModel.find({}, function(err, data, count){
                cb(null, data);
            });
        }, 
        ...
    }, 
    function(err, results) {
        callback(results);
    });
}

這邊用 find 函式來查詢資料庫的資料,並把得到的全部結果原封不動的存放到 results 去。藉由修改 find 的參數,我們也可以在這邊針對不同資料,自訂各項查詢條件。

改用 each 取得資料


使用 parallel 可以自訂要同步執行的不同工作,因此有著很大的彈性。但有時候我們所需要的只是針對不同資料執行相同的工作,用 parallel 必須要不斷的重複相同的程式碼(如上例中,不斷重複 find 的動作),看起來有些雜亂。

Async.js 另外提供其他流程控制與資料處理方法,其中一個方法是 each,它可以針對多筆資料平行處理相同的一件工作。例如以下範例,使用 each 來取得資料:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var findData = function(array, callback){
    var results = {};
    async.each(array, function(item, cb){
        var model = mongoose.model(item);
        model.find({},function(err, data, count){
            results[item] = data;
            cb();
        });
    },
    function(err){
        callback(results);
    });
}

findData(['dataA', 'dataB'], function(results){
    // results 物件的內容為 {'dataA': ... , 'dataB': ...}
    // 省略部分為資料庫中的資料
});

我們輸入一個參數 array,裡面存放不同 MongoDB collection 的名字,async.each 會去讀取這些 collection 名字,再利用 find 找到當中的所有資料,存放到物件變數 results 當中,最後呼叫 cb 結束。

結論


這篇文章簡單介紹 Async.js 裡的 parallel 與 each 兩種方法,並配合 mongoose 來達到平行處理多筆 MongoDB 查詢的方法。