J'ai lu plusieurs exemples d'utilisation de mysql dans node.js et j'ai des questions sur le traitement des erreurs.
La plupart des exemples traitent les erreurs de la manière suivante (peut-être par souci de concision):
app.get('/countries', function(req, res) {
pool.createConnection(function(err, connection) {
if (err) { throw err; }
connection.query(sql, function(err, results) {
if (err) { throw err; }
connection.release();
// do something with results
});
});
});
Cela provoque le blocage du serveur chaque fois qu’une erreur SQL est détectée. Je voudrais éviter cela et garder le serveur en marche.
Mon code est comme ça:
app.get('/countries', function(req, res) {
pool.createConnection(function(err, connection) {
if (err) {
console.log(err);
res.send({ success: false, message: 'database error', error: err });
return;
}
connection.on('error', function(err) {
console.log(err);
res.send({ success: false, message: 'database error', error: err });
return;
});
connection.query(sql, function(err, results) {
if (err) {
console.log(err);
res.send({ success: false, message: 'query error', error: err });
return;
}
connection.release();
// do something with results
});
});
});
Je ne suis pas sûr que ce soit la meilleure façon de le gérer. Je me demande également s'il devrait y avoir une connection.release()
dans le bloc err
. Sinon, les connexions pourraient rester ouvertes et se développer avec le temps.
Je suis habitué au code try...catch...finally
ou try-with-resources
de Java, qui me permet de "nettoyer" les erreurs et de fermer toutes mes ressources à la fin. Existe-t-il un moyen de propager les erreurs et de les gérer toutes au même endroit?
J'ai décidé de le gérer avec la syntaxe es2017 et avec Babel pour le transférer en es2016, ce que Node 7 prend en charge.
Les versions les plus récentes de Node.js supportent cette syntaxe sans transpiler.
Voici un exemple:
'use strict';
const express = require('express');
const router = express.Router();
const Promise = require('bluebird');
const HttpStatus = require('http-status-codes');
const fs = Promise.promisifyAll(require('fs'));
const pool = require('./pool'); // my database pool module, using promise-mysql
const Errors = require('./errors'); // my collection of custom exceptions
////////////////////////////////////////////////////////////////////////////////
// GET /v1/provinces/:id
////////////////////////////////////////////////////////////////////////////////
router.get('/provinces/:id', async (req, res) => {
try {
// get a connection from the pool
const connection = await pool.createConnection();
try {
// retrieve the list of provinces from the database
const sql_p = `SELECT p.id, p.code, p.name, p.country_id
FROM provinces p
WHERE p.id = ?
LIMIT 1`;
const provinces = await connection.query(sql_p);
if (!provinces.length)
throw new Errors.NotFound('province not found');
const province = provinces[0];
// retrieve the associated country from the database
const sql_c = `SELECT c.code, c.name
FROM countries c
WHERE c.id = ?
LIMIT 1`;
const countries = await connection.query(sql_c, province.country_id);
if (!countries.length)
throw new Errors.InternalServerError('country not found');
province.country = countries[0];
return res.send({ province });
} finally {
pool.releaseConnection(connection);
}
} catch (err) {
if (err instanceof Errors.NotFound)
return res.status(HttpStatus.NOT_FOUND).send({ message: err.message }); // 404
console.log(err);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send({ error: err, message: err.message }); // 500
}
});
////////////////////////////////////////////////////////////////////////////////
// GET /v1/provinces
////////////////////////////////////////////////////////////////////////////////
router.get('/provinces', async (req, res) => {
try {
// get a connection from the pool
const connection = await pool.createConnection();
try {
// retrieve the list of provinces from the database
const sql_p = `SELECT p.id, p.code, p.name, p.country_id
FROM provinces p`;
const provinces = await connection.query(sql_p);
const sql_c = `SELECT c.code, c.name
FROM countries c
WHERE c.id = ?
LIMIT 1`;
const promises = provinces.map(async p => {
// retrieve the associated country from the database
const countries = await connection.query(sql_c, p.country_id);
if (!countries.length)
throw new Errors.InternalServerError('country not found');
p.country = countries[0];
});
await Promise.all(promises);
return res.send({ total: provinces.length, provinces });
} finally {
pool.releaseConnection(connection);
}
} catch (err) {
console.log(err);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send({ error: err, message: err.message }); // 500
}
});
////////////////////////////////////////////////////////////////////////////////
// OPTIONS /v1/provinces
////////////////////////////////////////////////////////////////////////////////
router.options('/provinces', async (req, res) => {
try {
const data = await fs.readFileAsync('./options/provinces.json');
res.setHeader('Access-Control-Allow-Methods', 'HEAD,GET,OPTIONS');
res.setHeader('Allow', 'HEAD,GET,OPTIONS');
res.send(JSON.parse(data));
} catch (err) {
res.status(HttpStatus.INTERNAL_SERVER_ERROR).send({ error: err, message: err.message });
}
});
module.exports = router;
L'utilisation de async
/await
avec ce try { try { } finally { } } catch { } pattern
permet une gestion sans erreur des erreurs, où vous pouvez collecter et traiter toutes vos erreurs au même endroit. Le bloc finally ferme la connexion à la base de données quoi qu'il arrive.
Vous devez simplement vous assurer que vous tenez toutes vos promesses. Pour l’accès à la base de données, j’utilise le module promise-mysql
au lieu du module plain mysql
. Pour tout le reste, j'utilise le module bluebird
et promisifyAll()
.
J'ai également des classes d'Exception personnalisées que je peux lancer dans certaines circonstances, puis détecter celles qui se trouvent dans le bloc catch. Selon les exceptions pouvant être levées dans le bloc try, mon bloc catch pourrait ressembler à ceci:
catch (err) {
if (err instanceof Errors.BadRequest)
return res.status(HttpStatus.BAD_REQUEST).send({ message: err.message }); // 400
if (err instanceof Errors.Forbidden)
return res.status(HttpStatus.FORBIDDEN).send({ message: err.message }); // 403
if (err instanceof Errors.NotFound)
return res.status(HttpStatus.NOT_FOUND).send({ message: err.message }); // 404
if (err instanceof Errors.UnprocessableEntity)
return res.status(HttpStatus.UNPROCESSABLE_ENTITY).send({ message: err.message }); // 422
console.log(err);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send({ error: err, message: err.message });
}
pool.js:
'use strict';
const mysql = require('promise-mysql');
const pool = mysql.createPool({
connectionLimit: 100,
Host: 'localhost',
user: 'user',
password: 'password',
database: 'database',
charset: 'utf8mb4',
debug: false
});
module.exports = pool;
errors.js:
'use strict';
class ExtendableError extends Error {
constructor(message) {
if (new.target === ExtendableError)
throw new TypeError('Abstract class "ExtendableError" cannot be instantiated directly.');
super(message);
this.name = this.constructor.name;
this.message = message;
Error.captureStackTrace(this, this.contructor);
}
}
// 400 Bad Request
class BadRequest extends ExtendableError {
constructor(m) {
if (arguments.length === 0)
super('bad request');
else
super(m);
}
}
// 401 Unauthorized
class Unauthorized extends ExtendableError {
constructor(m) {
if (arguments.length === 0)
super('unauthorized');
else
super(m);
}
}
// 403 Forbidden
class Forbidden extends ExtendableError {
constructor(m) {
if (arguments.length === 0)
super('forbidden');
else
super(m);
}
}
// 404 Not Found
class NotFound extends ExtendableError {
constructor(m) {
if (arguments.length === 0)
super('not found');
else
super(m);
}
}
// 409 Conflict
class Conflict extends ExtendableError {
constructor(m) {
if (arguments.length === 0)
super('conflict');
else
super(m);
}
}
// 422 Unprocessable Entity
class UnprocessableEntity extends ExtendableError {
constructor(m) {
if (arguments.length === 0)
super('unprocessable entity');
else
super(m);
}
}
// 500 Internal Server Error
class InternalServerError extends ExtendableError {
constructor(m) {
if (arguments.length === 0)
super('internal server error');
else
super(m);
}
}
module.exports.BadRequest = BadRequest;
module.exports.Unauthorized = Unauthorized;
module.exports.Forbidden = Forbidden;
module.exports.NotFound = NotFound;
module.exports.Conflict = Conflict;
module.exports.UnprocessableEntity = UnprocessableEntity;
module.exports.InternalServerError = InternalServerError;
Une autre solution élégante consiste à utiliser async.series
et sa méthode de gestion des erreurs.
const mysql = require('mysql')
const async = require('async')
async.series([
function (next) {
db = mysql.createConnection(DB_INFO)
db.connect(function(err) {
if (err) {
// this callback/next function takes 2 optional parameters:
// (error, results)
next('Error connecting: ' + err.message)
} else {
next() // no error parameter filled => no error
}
})
},
function (next) {
var myQuery = ....
db.query(myQuery, function (err, results, fields) {
if (err) {
next('error making the query: ' + err.message)
return // this must be here
}
// do something with results
// ...
next(null, results) // send the results
})
},
function (next) {
db.close()
}],
//done after all functions were executed, except if it was an error
function(err, results) {
if (err) {
console.log('There was an error: ', err)
}
else {
//read the results after everything went well
... results ....
}
})
Cette fonction permet de retourner le pool disponible après une connexion MySQL réussie. Donc, avant de poursuivre toute requête, j'attendrai cette fonction pour vérifier si la connexion est correcte. Cela ne plantera pas le serveur même s'il n'y a pas de connexion à MySQL.
connect: function ()
{
return new Promise((resolve, reject) => {
let pool = Mysql.createPool({
connectionLimit: config.mysql.connectionLimit,
Host: config.mysql.Host,
user: config.mysql.user,
password: config.mysql.password,
database: config.mysql.database
});
pool.getConnection((err, con) =>
{
try
{
if (con)
{
con.release();
resolve({"status":"success", "message":"MySQL connected.", "con":pool});
}
}
catch (err)
{
reject({"status":"failed", "error":`MySQL error. ${err}`});
}
resolve({"status":"failed", "error":"Error connecting to MySQL."});
});
});
}
Paquet MySQL utilisé: https://www.npmjs.com/package/mysql
Native Promise async/wait ES2017
Je pense que vous pouvez faire quelque chose comme ça. Peu importe la procédure, la connexion sera libérée une fois la requête terminée et le serveur ne tombera pas en panne à cause de l'erreur.
var queryString = "SELECT * FROM notification_detail nd LEFT JOIN notification n ON nd.id_notification = n.uuid WHERE login_id = ? id_company = ?;";
var filter = [loginId, idCompany];
var query = connection.query({
sql: queryString,
timeout: 10000,
}, filter );
query
.on('error', function(err) {
if (err) {
console.log(err.code);
// Do anything you want whenever there is an error.
// throw err;
}
})
.on('result', function(row) {
//Do something with your result.
})
.on('end', function() {
connection.release();
});
Cela peut être une solution alternative beaucoup plus simple.
var query = connection.query({
sql: queryString,
timeout: 10000,
}, function(err, rows, fields) {
if (err) {
//Do not throw err as it will crash the server.
console.log(err.code);
} else {
//Do anything with the query result
}
connection.release()
});