node-sqlite3 中的事务

Transactions in node-sqlite3

node-sqlite3中,如果db当前处于序列化模式,下一条语句会在上一条语句的回调完成之前等待,还是与回调同时运行下一条语句?

使用 node-sqlite3 编写交易的最佳方式是什么?我考虑过这两种方法,但我不确定哪一种是正确的,或者即使它们都是错误的。

// NEXT DB STATEMENT WAITS FOR CALLBACK TO COMPLETE?
db.serialize(() => {

    db.run('BEGIN');

    // statement 1
    db.run(
        sql1,
        params1,
        (err) => {
            if (err) {
                console.error(err);
                return db.serialize(db.run('ROLLBACK'));
            }                           
        }
    );

    // statement 2
    db.run(
        sql2,
        params2,
        (err) => {
            if (err) {
                console.error(err);
                return db.serialize(db.run('ROLLBACK'));
            }

            return db.serialize(db.run('COMMIT));                               
        }
    );  
});



// NEXT DB STATEMENT DOES NOT WAIT FOR CALLBACK TO COMPLETE?
db.serialize(() => {

    db.run('BEGIN');

    // statement 1
    db.run(
        sql1,
        params1,
        (err) => {
            if (err) {
                console.error(err);
                return db.serialize(db.run('ROLLBACK'));
            }

            db.serialize(() => {

                // statement 2
                db.run(
                    sql2,
                    params2,
                    (err) => {
                        if (err) {
                            console.error(err);
                            return db.serialize(db.run('ROLLBACK'));
                        }

                        return db.serialize(db.run('COMMIT));                               
                    }
                );
            });                             
        }
    );
});

我敢说 db.serialize() 是一种不涉及任何魔法的便捷方法。应该可以通过等到一个语句完成后再发送下一个语句来序列化一批语句。

这也适用于交易,唯一必须保证的是没有其他写操作发生在同一个db 连接对象,同时语句 运行,以保持事务清洁(如 node-sqlite3 issue #304 的讨论线程中所述)。

链接将通过在前一个语句的回调中严格调用下一个语句来完成,除非前一个语句返回错误,此时应该停止执行。

这在通过 实际上 源代码中的堆栈回调完成时很笨拙。但是如果我们承诺 Database#run 方法,我们可以使用 promises:

const sqlite3 = require('sqlite3');

sqlite3.Database.prototype.runAsync = function (sql, ...params) {
    return new Promise((resolve, reject) => {
        this.run(sql, params, function (err) {
            if (err) return reject(err);
            resolve(this);
        });
    });
};

我们本可以依赖 util.promisify for the promisification, but this would result in the loss of one detail of the callback handling in Database#run (from the docs):

If execution was successful, the this object will contain two properties named lastID and changes which contain the value of the last inserted row ID and the number of rows affected by this query respectively.

我们的自定义变体捕获 this 对象并将其 returns 作为 promise 结果。

除此之外,我们可以定义一个经典的承诺链,从 BEGIN 开始,然后通过 Array#reduce 链接任意数量的语句,最后调用 COMMIT 成功或 ROLLBACK 错误:

sqlite3.Database.prototype.runBatchAsync = function (statements) {
    var results = [];
    var batch = ['BEGIN', ...statements, 'COMMIT'];
    return batch.reduce((chain, statement) => chain.then(result => {
        results.push(result);
        return db.runAsync(...[].concat(statement));
    }), Promise.resolve())
    .catch(err => db.runAsync('ROLLBACK').then(() => Promise.reject(err +
        ' in statement #' + results.length)))
    .then(() => results.slice(2));
};

因为这构建了 promise 链,它还构建了一个语句结果数组,它 returns 完成后(减去开头的两项,第一个是 undefined 来自 Promise.resolve(), 第二个是 BEGIN).

的结果

现在我们可以轻松地在隐式事务中传递多个语句以进行序列化执行。批处理的每个成员可以是一个独立的语句,也可以是一个带有语句和相关参数的数组(正如 Database#run 所期望的那样):

var statements = [
    "DROP TABLE IF EXISTS foo;",
    "CREATE TABLE foo (id INTEGER NOT NULL, name TEXT);",
    ["INSERT INTO foo (id, name) VALUES (?, ?);", 1, "First Foo"]
];

db.runBatchAsync(statements).then(results => {
    console.log("SUCCESS!")
    console.log(results);
}).catch(err => {
    console.error("BATCH FAILED: " + err);
});

这将记录如下内容:

SUCCESS!
[ { sql: 'DROP TABLE IF EXISTS foo;', lastID: 1, changes: 1 },
  { sql: 'CREATE TABLE foo (id INTEGER NOT NULL, name TEXT);',
    lastID: 1,
    changes: 1 },
  { sql: 'INSERT INTO foo (id, name) VALUES (?, ?);',
    lastID: 1,
    changes: 1 } ]

万一出现错误,这将导致回滚,我们将从数据库引擎取回错误消息,加上 "in statement #X" 其中 X 指的是到批处理中的语句位置。