Most database APIs have a way of dealing with transactions - a set of operations all of which much work, and a way of rolling back if they don't Indeed many of the backends implemented here have this capability, but I haven't yet exposed that through the abstraction layer. The main reason for this is that many of the backends are not actually databases, and therefore don't have a roll back capability or the concept of transactions. However since I had always planned to introduce rollback, I've implemented these kind of databases to use DriverMemory first, before committing to the backend. This means that I can now start to implement transactions whether simulated, or using the transactional capabilities of the backend. Here's how it works.
Some notes on performance
You can set locking when you open the database - for example. var dbParameters = { "siloid": "multi", "dbid": "1yTQFdN_O2nFb9obm7AHCTmPKpf5cwAd78uNQJiCcjPk", "transactions": cDbAbstraction.dhConstants.TRANSACTIONS.ENABLED, "locking" : cDbAbstraction.dhConstants.LOCKING.AGGRESSIVE }; values for locking are cDbAbstraction.dhConstants.LOCKING.AGGRESSIVE cDbAbstraction.dhConstants.LOCKING.ENABLED (default) cDbAbstraction.dhConstants.LOCKING.DISABLED values for transactions are cDbAbstraction.dhConstants.TRANSACTIONS.ENABLED (default) cDbAbstraction.dhConstants.TRANSACTIONS.DISABLED If you have some code wrapped in transactions, and you disable transactions (or the driver does not yet support them), then each operations will be performed as today, but following the setting for locking You wrap operations in a transaction like this var result = dbHandler.transaction ( function(db) { // your related activities }, options ); Sheets are a particular problem, since they don't have stable IDs (row numbers are used as IDs), and since the design principle here is not to do anything that affects the sheet's standalone nature (anybody can change it at any time), the row number of an item can change at any time. It's especially important to wrap operations that rely on previous operations inside a transaction. Below is a query, where the keys are used by a subsequent get. Wrapping the entire thing in a transaction ensure that some other user does not affect the row numbers in between var r2 = handler.transaction ( function(db,options) { var result = handler.query ( { strain:strain, 'stuff.age': handler.constraints([[c.GT,25]]) },{limit:1}, 1, true ); assert ([ result.handleCode >= 0, result.data.length ===1, result.handleKeys.length ===1 ], result, "limitkeycheck1"); // testing Get var r2 = handler.get(result.handleKeys); assert ([ r2.handleCode >= 0, r2.data.length === result.data.length, r2.data.every (function(d) { return d.stuff.age > 25 ; }) ], r2, "getcheck1"); // status and result to be handled by transaction wrapper return r2; }); assert(r2.transaction.code >=0, r2, 'transaction 1'); another example - this time we are doing an update. It's important that the row number IDs dont change between the query that finds them, and the subsequent update, so wrapping them in a transaction ensures this. var r2 = handler.transaction ( function(db,options) { var result = db.query ( { strain:strain, 'stuff.sex': 'male' },undefined,1,true); assert ([ result.handleCode >= 0, testData.filter(function(d) { return (d.stuff.sex === 'male') ; }).length === result.handleKeys.length ], result, "does male work"); var r2 = db.update( result.handleKeys, result.data.map (function(d) { d.stuff.man = d.stuff.sex === 'male'; return d; })); assert (r2.handleCode >= 0, r2, 'real update'); return r2; }); assert(r2.transaction.code >=0, r2, 'transaction 2'); What handler.transaction() does/** * transaction wrapper * @param {function} transactionFunction things that need to happen inside a transaction * @param {object} options any options you want to pass on to your transaction function * @return {object} a normal result package */ this.transaction = function (transactionFunction , options) { // get id of current transaction if any var id = self.getTransactionId(); // if there was already one under way that's a bad thing if (id) { // report a transaction within a transaction as an error var results= makeResults (enums.CODE.TRANSACTION_FAILURE); results.transaction = { id: id, code: enums.CODE.OK, error:enums.ERROR.TRANSACTION_FAILURE }; results.transaction = transaction; return results; } // set up new transaction var transaction = { id: self.setTransaction(), code: enums.CODE.OK, error:'' }; // the rules // if a driver is transaction aware // it knows about locking regime of a transaction // the entire transaction will be locked // the items of the transaction are forbidden to do any locking // further, a transaction capable driver will be able to do rollback etc. // for non-aware drivers, a transaction is a non event - nothing happens, the parts of it are executed normally // if transactions are disabled, then all drivers are treated as non-transaction aware var transactionAware = driver.transactionAware && self.transactionsState() === enums.TRANSACTIONS.ENABLED; var transactionCapable = transactionAware && driver.transactionCapable; // if locking is disabled then no locking is done on the transaction var lockingEnabled = self.lockingState() !== enums.LOCKING.DISABLED && !driver.lockingBypass; var transactionLockBypass = !transactionAware || !lockingEnabled; // enclose the whole transaction var result = doGuts_ ( transactionLockBypass , "transaction:"+transaction.id , function (bypass) { // only applies to drivers that are able to do transactions if (transactionCapable) { // let the driver know it's doing a transaction driver.beginTransaction(transaction.id); // do the work try { // get the current state data, and execute the transaction contents driver.transactionData(); var r = transactionFunction (self , options); if (r.handleCode < 0 ) { localRollBack(); } else { // commit the transaction if (driver.getTransactionBox().dirty) { self.voidCache(); } var r = driver.commitTransaction(transaction.id); transaction.code = r.handleCode; transaction.error = r.handleError; } } catch (err) { localRollBack(); var r = self.makeResults(enums.CODE.TRANSACTION_FAILURE, err); } return r; } else { // the function is executed normally. // the driver should detect whether it is part of a transaction and act accordingly transaction.code = enums.CODE.TRANSACTION_INCAPABLE; transaction.error = self.getErrorText(transaction.code); // do the thing return transactionFunction (self , options); } }); // mark transaction as over result.transaction = transaction; self.clearTransaction(); return result; function localRollBack() { var r = driver.rollbackTransaction(transaction.id); transaction.code = r.handleCode < 0 ? enums.CODE.TRANSACTION_ROLLBACK_FAILED : enums.CODE.TRANSACTION_ROLLBACK; transaction.error = self.getErrorText(transaction.code); self.voidCache(); } }; What an enabled driver will do
self.save = function (obs) { return delegate.save(self.unFlatten(obs)); }; However sheets, properties and drive can delegate all their workload to the memory driver in a transaction, since there will be no other updates going on. This makes things much more efficient and also means that they can share the delegated code which looks like this What a failed transaction looks like Here's a deliberate error in a transaction - i've spelt remove wrongly, so I want it to rollback the save operation. var result = handler.transaction (function(db) { db.save([ {name:'he',ob:{a:'a1',b:'b1'}}, {name:'she',ob:{a:'ax',b:'b2'}} ]); return db.remov({name:'me'}); }); and the result{ "handleCode": -24, "handleError": "(DbAbstraction says:TRANSACTION_FAILURE) (TypeError: Cannot find function remov in object [object Object].) some transaction failure", "data": [], "handleVersion": "cDbAbstraction:2.1.1", "driverVersion": "cDriverMemory:2.1.0", "table": "multi", "dbId": "multi", "transaction": { "id": "xiui3bg5f0f", "code": -22, "error": "transaction failed, but rolled back successfully" } }
A note on keysAs previously noted, some drivers don't have the capability to store unique keys. In this case a unique key is generated for them temporarily. This is how the operations within a transaction can communicate with each other. This means that you can't always rely on keys being the same inside and outside transactions. If you are dealing with keys (for example get/update), it's always best to wrap those in a transaction. For example, with a sheet driver... var result = handler.query({name:'john'},undefined, undefined, true); result.handleKeys , will contain row numbers at that point in time - which may change at any time by other user operations, so they should not be relied on - so the following may not return the right data (it will detect it has changed and give you an error)var r2 = handler.get (result.handleKeys) whereas handler.transaction ( function (db) { var result = handler.query({name:'john'},undefined, undefined, true); var r2 = handler.get (result.handleKeys); }); will return the correct value since the operations wont be interrupted mid flight by another update instance. Therefore, if you are using keys for database backends without a unique key capability, you should keep them isolated to within transactions. DbAbstraction full code
|
Services > Desktop Liberation - the definitive resource for Google Apps Script and Microsoft Office automation > Database abstraction and Google Apps Script >