|
2. Persistency
2.4 Minimising Clashes
The most elegant approach of all to update clashes is of course to avoid them completely.
Handling Clashes Automatically
Update clashes are handled automatically wherever possible. When a version change is detected, ReStore will compare three versions of the object:
the object as it was prior to the current change (version 1)
the object as it is currently stored in the database (version 2)
the object as it is in memory, following the change (version 2' )
If the changes made in the database (1>>2) are independent of the changes made in memory (1>>2'), ReStore will avoid an update clash by refreshing 2' with the changes made between 1 and 2. It will then commit this merged version of the object, giving version 3. For example:
Time User A Version User B Version Database Version 00:00 Begin editing Person 'John Smith' 1 'John Smith' 1 00:01 Begin editing Person 'John Smith' 1 00:09 Change surname to 'Smythe' 2 00:10 Change firstName to 'James' 2' 00:14 Commit Transaction 2 'John Smythe' 2 00:15 Commit Transaction (update clash detected) 2' 'John Smythe'
2 00:15 Merge Changes, recommit (succeeds) 3 'James Smythe' 3
Handling Clashes Manually
If, however, there is a 'full' update clash (i.e. the changes 1>>2 and 1>>2' affect one or more of the same instance variables), then ReStore will ask the changed object itself to try to resolve the change. ReStore does this by sending the message handleUpdateClash:from:to: to the affected object(s) for each clashing instance variable - the arguments to the message being the changed instance variable (a Symbol), the previous version of the object (version 1) and the current database version of the object (version 2).By implementing handleUpdateClash:from:to: in your model classes, you can update the in-memory version of the object (version 2') to resolve the clash, where possible. The result of this method should be a Boolean indicating whether the clash was resolved. If all update-clashing objects in a transaction are able to resolve their own clashes, then the transaction will commit successfully with no further intervention.
A good example of handleUpdateClash:from:to: can be seen in the Entertainment Shop example application, in class StockItem. The most volatile attribute of a StockItem is likely to be its numberInStock, since this will change every time an order is made for that item. A very popular item is likely to be ordered by many users at one time, and so an update clash on numberInStock is very likely. StockItem attempts to resolve this potential update clash itself as follows:
handleUpdateClash: aSymbol from: oldVersion to: newVersion
"This method only handles stock level clashes"
(aSymbol = #numberInStock) ifFalse: [^false].myStockChange := self numberInStock - oldVersion numberInStock.
newStockLevel := newVersion numberInStock + myStockChange."Can only resolve the clash if this would not lead to a negative numberInStock"
^newStockLevel >= 0
ifTrue: [self numberInStock: newStockLevel. true]
ifFalse: [false]Firstly, the method checks whether the clashing attribute is actually numberInStock. If not (e.g. two users have changed title), it makes no further attempt to handle the clash.
Next, the relative change in numberInStock between version1 (oldVersion) and version 2' (self) is calculated. The method then applies this change of stock level to the numberInStock as currently stored in the database (newVersion), to give the new, merged, stock level.
Finally, the method checks that the merged stock level change has not led to a negative numberInStock - this would obviously be invalid, and cannot be handled. Assuming this is not the case, however, the merged numberInStock is applied to the StockItem, and true is returned to denote that the update clash has been handled successfully.
By targeting implementations of handleUpdateClash:from:to: at situations where update clashes are likely, you can improve the usability of your applications by ensuring that most transactions commit successfully on the first attempt.