web-dev-qa-db-ja.com

Node.jsを使用した同期データベースクエリ

ルート内のMySQL dbを照会し、結果をユーザーに表示するNode.js/Expressアプリがあります。私の問題は、ユーザーを要求したページにリダイレクトする前に、両方のクエリが完了するまでクエリを実行してブロックする方法です。

この例では、ページをレンダリングする前に完了する必要がある2つのクエリがあります。クエリ1の「結果」コールバック内にクエリ2をネストすると、クエリを同期して実行できます。ただし、クエリの数が増えると、これは非常に複雑になります。

前のクエリの「結果」コールバックに後続のクエリをネストせずに、複数の(この場合は2)データベースクエリを同期的に実行するにはどうすればよいですか?

Nodeモジュールで 'フロー制御/非同期グッズ'を見て、flow-jsを試しましたが、非同期クエリで動作させることができません。

以下は、「/ home」ルートから実行しようとしている2つのクエリです。 Nodeエキスパートがこれを行う「正しい」方法を説明できますか。

app.get('/home', function (req,res) {
    var user_array = [];
    var title_array = [];

    // first query
    var sql = 'select user_name from users';
    db.execute(sql)
        .addListener('row', function(r) {
            user_array.Push( { user_name: r.user_name } );
        })
        .addListener('result', function(r) {
            req.session.user_array = user_array;
        });

    // second query
    var sql = 'select title from code_samples';
    db.execute(sql)
        .addListener('row', function(r) {
            title_array.Push( { title: r.title } );
        })
        .addListener('result', function(r) {
            req.session.title_array = title_array;
        });

        // because the queries are async no data is returned to the user
        res.render('home.ejs', {layout: false, locals: { user_name: user_array, title: title_array }});
});
52
Rick

ノードの目標は、物事がどのような順序で発生するかを気にしないことです。これにより、いくつかのシナリオが複雑になる可能性があります。ネストコールバックに恥はありません。外観に慣れると、実際にそのスタイルを好むことがあります。私がやります;コールバックが起動する順序は非常に明確です。匿名関数は、必要に応じて冗長にすることを控えることができます。

コードを少し再構築する場合は、「典型的な」ネストされたコールバックメソッドを使用できます。コールバックを回避したい場合、これを行うのに役立つ多くの非同期フレームワークがあります。チェックアウトしたいのはasync.js(https://github.com/fjakobs/async.js)です。それぞれの例:

app.get('/home', function (req,res) {
    var lock = 2;
    var result = {};
    result.user_array = [];
    result.title_array = [];

    var finishRequest = function(result) {
        req.session.title_array = result.title_array;
        req.session.user_array = result.user_array;
        res.render('home.ejs', {layout: false, locals: { user_name: result.user_array, title: result.title_array }});
    };

    // first query
    var q1 = function(fn) {
      var sql = 'select user_name from users';
      db.execute(sql)
          .addListener('row', function(r) {
              result.user_array.Push( { user_name: r.user_name } );
          })
          .addListener('result', function(r) {
              return fn && fn(null, result);
        });
    };

    // second query
    var q2 = function(fn) {
      var sql = 'select title from code_samples';
      db.execute(sql)
          .addListener('row', function(r) {
              result.title_array.Push( { title: r.title } );
          })
          .addListener('result', function(r) {
              return fn && fn(null, result);
          });
    }

    //Standard nested callbacks
    q1(function (err, result) {
      if (err) { return; //do something}

      q2(function (err, result) {
        if (err) { return; //do something}

        finishRequest(result);
      });
    });

    //Using async.js
    async.list([
        q1,
        q2,
    ]).call().end(function(err, result) {
      finishRequest(result);
    });

});

一度限りの場合、おそらく参照カウント型のアプローチを使用します。実行するクエリの数を追跡し、すべてのクエリが終了したときに応答をレンダリングするだけです。

app.get('/home', function (req,res) {
    var lock = 2;
    var user_array = [];
    var title_array = [];

    var finishRequest = function() {
        res.render('home.ejs', {layout: false, locals: { user_name: user_array, title: title_array }});
    }

    // first query
    var sql = 'select user_name from users';
    db.execute(sql)
        .addListener('row', function(r) {
            user_array.Push( { user_name: r.user_name } );
        })
        .addListener('result', function(r) {
            req.session.user_array = user_array;
            lock -= 1;

            if (lock === 0) {
              finishRequest();
            }
        });

    // second query
    var sql = 'select title from code_samples';
    db.execute(sql)
        .addListener('row', function(r) {
            title_array.Push( { title: r.title } );
        })
        .addListener('result', function(r) {
            req.session.title_array = title_array;
            lock -= 1;

            if (lock === 0) {
              finishRequest();
            }
        });
});

さらに良い方法は、応答をレンダリングする前に、各「結果」コールバックで単純にfinishRequest()を呼び出して、空でない配列をチェックすることです。それがあなたのケースで機能するかどうかは、要件によって異なります。

54
jslatts

複数のコールバックを処理するための非常に簡単なトリックを次に示します。

var after = function _after(count, f) {
  var c = 0, results = [];
  return function _callback() {
    switch (arguments.length) {
      case 0: results.Push(null); break;
      case 1: results.Push(arguments[0]); break;
      default: results.Push(Array.prototype.slice.call(arguments)); break;
    }
    if (++c === count) {
      f.apply(this, results);
    }
  };
};

使用法:

var handleDatabase = after(2, function (res1, res2) {
  res.render('home.ejs', { locals: { r1: res1, r2: res2 }):
})

db.execute(sql1).on('result', handleDatabase);
db.execute(sql2).on('result', handleDatabase);

したがって、基本的には参照カウントが必要です。これは、これらの状況での標準的なアプローチです。実際に、フロー制御の代わりにこの小さなユーティリティ関数を使用しています。

本格的なフロー制御ソリューションが必要な場合は、 futuresJS をお勧めします

17
Raynos

私は非同期ライブラリがこのようなものに最適であることがわかりました。 https://github.com/caolan/async#parallel

これをテストすることはできませんので、タイプミスがある場合はご容赦ください。クエリ関数を再利用可能にリファクタリングしました。したがって、queryRowsを呼び出すと、非同期モジュールの並列コールバック関数の形式に一致する関数が返されます。両方のクエリが完了すると、最後の関数が呼び出され、2つのクエリの結果が引数として渡されます。この引数を読み取ってテンプレートに渡すことができます。

function queryRows(col, table) {
  return function(cb) {
    var rows = [];
    db.execute('SELECT ' + col + ' FROM ' + table)
      .on('row', function(r) {
        rows.Push(r)        
      })
      .on('result', function() {
        cb(rows);
      });
  }
}

app.get('/home', function(req, res) {
  async.parallel({
    users: queryRow('user_name', 'users'),
    titles: queryRow('title', 'code_samples')
  },
  function(result) {
    res.render('home.ejs', { 
      layout: false,
      locals: {user_name: result.users, title: result.titles} 
    });
  });
});
14
loganfsmyth

ここにはいくつかの解決策がありますが、私の意見では、最良の解決策は非常に簡単な方法でコードを同期的に作成することです。

「synchonize」パッケージを使用できます。

ただ

npm install synchronize

次にvar sync = require(synchronize);

を使用してファイバーに同期するロジックを配置します

sync.fiber(function() { //put your logic here }

2つのmysqlクエリの例:

var express = require('express');
var bodyParser = require('body-parser');
var mysql = require('mysql');
var sync = require('synchronize');

var db = mysql.createConnection({
    Host     : 'localhost',
    user     : 'user',
    password : 'password',
    database : 'database'
});

db.connect(function(err) {
    if (err) {
        console.error('error connecting: ' + err.stack);
        return;
    }
});

function saveSomething() {
    var post  = {id: newId};
    //no callback here; the result is in "query"
    var query = sync.await(db.query('INSERT INTO mainTable SET ?', post, sync.defer()));
    var newId = query.insertId;
    post  = {foreignKey: newId};
    //this query can be async, because it doesn't matter in this case
    db.query('INSERT INTO subTable SET ?', post, function(err, result) {
        if (err) throw err;
    });
}

「saveSomething()」が呼び出されると、メインテーブルに行を挿入し、最後に挿入されたIDを受け取ります。その後、以下のコードが実行されます。約束やそのようなものをネストする必要はありません。

3
EscapeNetscape

Node.JSでファイバーを使用して擬似同期コードを記述できます。DBのこれらのテストをご覧ください https://github.com/alexeypetrushin/mongo-lite/blob/master/test/collection.coffee 非同期ですが、同期のように見えます。詳細 http://alexeypetrushin.github.com/synchronize

1

オプション1:すべてのクエリが相互に関連している場合、ストアドプロシージャを作成し、すべてのデータロジックをその中に入れて、単一のdb.execute

オプション2:データベースが1つの接続を使用する場合、シリアルで実行されることが保証されているコマンドで、これを非同期ヘルパーとして使用できます

db.execute(sql1).on('row', function(r) {
   req.session.user_array.Push(r.user);
});
db.execute(sql2)
.on('row', function(r) {
   req.session.title_array.Push(r.title);
})
.on('end'), function() {
   // render data from req.session
});
1
Andrey Sidorov