In node-sqlite3
, if the db is currently in serialized mode, will the next statement wait before the callback of the previous statement finishes, or will the callback run at the same time as the next statement?
What's the best way to write a transaction using node-sqlite3
? I've thought about these two approaches, but I'm not sure which one is correct, or even if they're both wrong.
// 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));
}
);
});
}
);
});
SQLite supports multiple simultaneous read transactions coming from separate database connections, possibly in separate threads or processes, but only one simultaneous write transaction. A read transaction is used for reading only. A write transaction allows both reading and writing.
SQLite is a transactional database that all changes and queries are atomic, consistent, isolated, and durable (ACID). SQLite guarantees all the transactions are ACID compliant even if the transaction is interrupted by a program crash, operation system dump, or power failure to the computer.
Node. js can be used very well with relational databases and SQLite is no exception.
"Nested Android Transactions" do not use SQLites nested transaction/savepoint support. Rather a nested Android transaction suppresses manifesting a SQLite transaction. The nested transaction cannot be rolled back itself, because it does not exist apart from the outside transaction.
I'm going out on a limb and say that db.serialize()
is a convenience method that does not involve any magic. It should be possible to serialize a batch of statements by waiting until one finishes before sending the next.
This would also work for transactions, the only thing that must be guaranteed is that no other writes happen to the same db
connection object while the statements are being run, to keep the transaction clean (as noted in the discussion thread to node-sqlite3 issue #304).
Chaining would be done by strictly calling the next statement in the callback of the previous one, unless the previous one has returned an error, at which point the execution should be stopped.
This is unwieldy when done by actually stacking callbacks in the source code. But if we promisify the Database#run
method, we can use 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);
});
});
};
We could have relied on 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 namedlastID
andchanges
which contain the value of the last inserted row ID and the number of rows affected by this query respectively.
Our custom variant captures the this
object and returns it as the promise result instead.
With that out of the way, we can define a classic promise chain, kicking off with BEGIN
, then chaining in any number of statements via Array#reduce
, and eventually calling COMMIT
on success or ROLLBACK
on error:
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));
};
As this builds the promise chain, it also builds an array of statement results that it returns when done (minus two items at the start, the first one being undefined
from Promise.resolve()
, the second one being the result of BEGIN
).
Now we can easily pass several statements for serialized execution inside an implicit transaction. Each member of the batch may be either a stand-alone statement, or an array with statement and associated parameters (just as Database#run
would expect it):
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);
});
which would log something like this:
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 case of an error, this would cause a roll back, and we would get back the error message from the DB engine, plus "in statement #X" where X refers to the statement position in the batch.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With