2.3 Multi-User Transactions
Note: the advanced multi-user facilities detailed in sections 2.3 and 2.4 are not included in the free trial version.
As discussed so far, transactions are suitable for single user applications - where there is only one user logged into the database at any one time. If more than one user is connected, then there is the possibility that two or more users may attempt to change the same object around the same time. This may lead to an update clash when a transaction is committed. Consider:
Time User A User B Database 00:00 Begin editing Person 'John Smith' 'John Smith' 00:01 Begin editing Person 'John Smith' 'John Smith' 00:09 Change surname to 'Smythe' 'John Smith' 00:10 Change surname to 'Smithe' 'John Smith' 00:14 Commit Transaction 'John Smythe' 00:15 Commit Transaction
What should happen when User B commits their transaction? If the transaction is allowed to commit, then User A's changes will be overwritten. If the transaction fails, then User B will lose their changes - is this acceptable?
This section details how to detect and handle such update clashes.
In Chapter 1, we showed how to begin using ReStore:
reStore := SSWReStore new.
useTransactionsWithoutVersioning means that individual objects within the database do not track changes to themselves - this is acceptable for a single-user scenario. However, in a multi-user environment, each object needs to note its version in order that update clashes can be detected. This can be enabled by instructing ReStore as follows:
When versioning is enabled, all tables in the database have an additional column to store the Version Number of each object. This number is invisible to your objects as you see them in your image, but is used by the transaction mechanism to note changes in version. In the above example, let's say that the Person object 'John Smith' is at version 1 when Users A and B begin their operation. When User A's transaction commits, the version number will be increased to 2. Thus when User B's transaction attempts to commit, it will be able to see that the version in the database (2) is not the same as the version in memory (1). Thus the update clash can be detected.
Since useTransactionsWithVersioning adds an additional column to each table, it is technically a change to the structure of all persistent classes. Hence if you begin developing a single-user application (without versioning) and later switch to multi-user mode (with versioning), you will need to re-synchronize your data model.
It is not recommended to switch an application from multi-user mode to single-user mode.
With versioning enabled, it is possible for update clashes to be detected. The next issue is how to deal with them.
The immediate response to an update clash is that the transaction will fail to commit. To check for this, you must test the result of commitTransaction - this will return a Boolean to indicate whether the transaction succeeded (true) or failed (false).
Note that, when a transaction fails to commit you have not (yet) lost the changes made during that failed transaction - they still exist in the transaction, and the changed objects retain their changed state in memory. However, none of the changes have been committed to the database - an update clash on one single object prevents the whole transaction from committing.
Following a failed commitTransaction you may ask ReStore which objects are the cause of the problem:
This message will return a collection of the objects which are involved in the update clash - you can use this to decide between the approaches detailed below, or simply in order to give enhanced feedback to the user.
Rollback and Refresh
The simplest action to take is to fail the whole transaction, rollback the changes made and update the clashing object(s) with the latest versions from the database. This is done by simply instructing ReStore as follows
Following a rollbackAndRefreshTransaction, you could (for example) issue a notification of the clash to the user ("Another user has changed the data you were editing..."), present them with the refreshed objects and ask them to repeat their actions.
Refresh and Rollforward
An alternative approach is to merge the changes in the transaction with the changes in the database. This is done as follows:
With refreshAndRollforwardTransaction, the object(s) causing the update clash are refreshed with their latest versions from the database, and the changes made in the transaction are re-applied onto the refreshed objects. Following a refreshAndRollforwardTransaction, you will need to re-commitTransaction, remembering that this second commit may also fail if any objects have been changed further since the refresh.
Using refreshAndRollforwardTransaction avoids the user having to repeat their changes, but you should bear in mind that this also allows the user to overwrite another user's changes without having first viewed them.