【问题标题】:Transactions in node-sqlite3node-sqlite3 中的事务
【发布时间】:2019-04-17 08:49:10
【问题描述】:

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));                               
                    }
                );
            });                             
        }
    );
});

【问题讨论】:

  • “下一条语句会在前一条语句的回调完成之前等待” - 请参阅文档。 github.com/mapbox/node-sqlite3/wiki/…
  • @Tomalak 我做了,但文档没有提到回调会发生什么。它只是说“请注意,不是直接在回调函数中安排的查询不一定是序列化的”,这并没有回答我原来的问题。
  • 嗯,我明白了。我看起来这个确切的问题已经在node-sqlite3 issue #304 中讨论过了,但是这个帖子很旧而且没有定论。
  • 我很好奇:您在使用我提出的解决方案吗?它是否按预期工作?
  • @Tomalak 虽然我并没有完全使用它,但它确实对我有很大帮助。我已经处于著名的回调地狱中,因此现在更改整个代码库以使用 Promise 将是太多了。因此,我在回答开始时遵循了您的建议:为每个新事务打开一个序列化模式的新数据库连接,并在完成后关闭,从而保证不会对同一连接对象发生其他写入。

标签: node.js node-sqlite3


【解决方案1】:

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

这也适用于事务,唯一必须保证的事情是,在语句执行期间,没有其他写入发生在同一个 db 连接对象上运行,以保持事务干净(如node-sqlite3 issue #304 的讨论线程中所述)。

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

当通过实际上在源代码中堆叠回调来完成时,这很笨拙。但是如果我们promisify 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 来实现承诺,但这会导致Database#run(来自the docs)中callback 处理的一个细节丢失:

如果执行成功,this 对象将包含两个名为 lastIDchanges 的属性,它们分别包含最后插入的行 ID 的值和受此查询影响的行数。

我们的自定义变体捕获this 对象并将其作为承诺结果返回。

除此之外,我们可以定义一个经典的 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.resolve() 中的 undefined,第二项是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);
});

这会记录这样的事情:

成功! [ { sql: 'DROP TABLE IF EXISTS foo;', lastID: 1, changes: 1 }, { sql: 'CREATE TABLE foo (id INTEGER NOT NULL, name TEXT);', 最后一个ID:1, 变化:1}, { sql: 'INSERT INTO foo (id, name) VALUES (?, ?);', 最后一个ID:1, 变化:1}]

如果发生错误,这将导致回滚,我们将从数据库引擎中获取错误消息,加上 "in statement #X" 其中 X 指的是语句位置在批处理中。

【讨论】:

    猜你喜欢
    • 2011-11-17
    • 2023-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-09
    • 2019-10-14
    • 1970-01-01
    • 2021-06-19
    相关资源
    最近更新 更多