SQLite sur Node.js avec async/await

Interface pour rendre chaque fonction de SQLite synchrone et les utiliser avec await.

Requiert le module SQLite 3 pour Node et Node.js 8.0 qui supporte async/await.

SQLite est plus couramment utilisé comme moyen de stockage de données pour des applications locales et mobiles, aussi le style asynchrone pour lire et écrire dans la base, et les callbacks, ne sont pas la meilleure solution pour accéder au données dans les différentes parties du programme.

Pour accéder aux données plus naturellement dans un programme procédural, j'ai écrit une interface qui convertit les callbacks en promises, afin que l'on puisse utiliser chaque fonction avec le mot réservé await.

Il ne s'agit pas d'une alternative au mode asnychone, mais plutôt d'un complément: vous pouvez utiliser les fonctions asynchrones et synchrones ensemble.

Le module aa-sqlite

L'interface à SQLite est un module nommé aa-sqlite, vous pouvez le placer dans la section node_modules de votre application. En voici le code source:

const sqlite3 = require('sqlite3').verbose()
var db

exports.db = db

exports.open=function(path) {
    return new Promise(function(resolve) {
    this.db = new sqlite3.Database(path, 
        function(err) {
            if(err) reject("Open error: "+ err.message)
            else    resolve(path + " opened")
        }
    )   
    })
}

// any query: insert/delete/update
exports.run=function(query) {
    return new Promise(function(resolve, reject) {
        this.db.run(query, 
            function(err)  {
                if(err) reject(err.message)
                else    resolve(true)
        })
    })    
}

// first row read
exports.get=function(query, params) {
    return new Promise(function(resolve, reject) {
        this.db.get(query, params, function(err, row)  {
            if(err) reject("Read error: " + err.message)
            else {
                resolve(row)
            }
        })
    }) 
}

// set of rows read
exports.all=function(query, params) {
    return new Promise(function(resolve, reject) {
        if(params == undefined) params=[]

        this.db.all(query, params, function(err, rows)  {
            if(err) reject("Read error: " + err.message)
            else {
                resolve(rows)
            }
        })
    }) 
}

// each row returned one by one 
exports.each=function(query, params, action) {
    return new Promise(function(resolve, reject) {
        var db = this.db
        db.serialize(function() {
            db.each(query, params, function(err, row)  {
                if(err) reject("Read error: " + err.message)
                else {
                    if(row) {
                        action(row)
                    }    
                }
            })
            db.get("", function(err, row)  {
                resolve(true)
            })            
        })
    }) 
}

exports.close=function() {
    return new Promise(function(resolve, reject) {
        this.db.close()
        resolve(true)
    }) 
}

La méthode get retourne une ligne de la base, tandis que all retourne un tableau de lignes.

Dans le cas de each, c'est plus compliqué parcer que SQLite appelle une fonction en callback pour chaque ligne qui répond à la condition de la requête. La solution est d'utiliser Database.serialize pour les traiter l'une après l'autre, et ensuite appeller une ultime méthode get vide pour résoudre la promise.

Démonstration

Cette démo donne un exemple d'utilisation de chaque fonction de aa-sqlite. Dans la première partie on ouvre la base, ajoute une table et la remplit avec quelques lignes. Puis la base est fermée, on l'ouvre de nouveau pour effectuer quelques requêtes synchrones.

const fs = require("fs")
const sqlite = require("aa-sqlite")

async function mainApp() {
    
    console.log(await sqlite.open('./users.db'))
    
    // Adds a table
    
    var r = await sqlite.run('CREATE TABLE users(ID integer NOT NULL PRIMARY KEY, name text, city text)')
    if(r) console.log("Table created")

    // Fills the table
    
    let users = {
        "Naomi": "chicago",
        "Julia": "Frisco",
        "Amy": "New York",
        "Scarlett": "Austin",
        "Amy": "Seattle"
    }
    
    var id = 1 
    for(var x in users) {
        var entry = `'${id}','${x}','${users[x]}'`
        var sql = "INSERT INTO users(ID, name, city) VALUES (" + entry + ")"
        r = await sqlite.run(sql)
        if(r) console.log("Inserted.")
        id++        
    }

    // Starting a new cycle to access the data

    await sqlite.close();
    await sqlite.open('./users.db') 

    console.log("Select one user:")
    
    var sql = "SELECT ID, name, city FROM users WHERE name='Naomi'"
    r = await sqlite.get(sql)
    console.log("Read:", r.ID, r.name, r.city)
    
    console.log("Get all users:")
    
    sql = "SELECT * FROM users"
    r = await sqlite.all(sql, [])
    r.forEach(function(row) {
        console.log("Read:", row.ID, row.name, row.city)    
    })
    
    console.log("Get some users:")
    
    sql = "SELECT * FROM users WHERE name=?"
    r = await sqlite.all(sql, ['Amy'])
    r.forEach(function(row) {
        console.log("Read:", row.ID, row.name, row.city)    
    })

    console.log("One by one:")
    
    sql = "SELECT * FROM users"
    r = await sqlite.each(sql, [], function(row) {
        console.log("Read:", row.ID, row.name, row.city)    
    })

    if(r) console.log("Done.")

    sqlite.close();
}

try {
    fs.unlinkSync("./users.db")
}
catch(e) {
}

mainApp()

Puisque la méthode all retourne un tableau, on utilise forEach pour traiter le contenu de chaque ligne.

Vous pouvez vérifier spécialement dans le cas de la méthode each, que chaque ligne retournée est traitée avant que le programme n'affiche "Done". Ce ne serait pas le cas avec les méthodes originales asynchrones.

Vous pouvez télécharger une archive contenant le module aa-sqlitet et le démo.

Il vous reste encore à installer SQLite 3 avant de lancer la démo.