Transactions are a nice to have feature, however, they are not a must have feature. Starbucks doesn’t use a two phase commit and MySql became the most popular open source database in existence long before they added transactions as a feature.
In SandstoneDB, concurrency is handled by calling either
critical: and it’s entirely up to the programmer to put critical sections around the appropriate code. You are working on the same instances of these objects as other threads and you need to be aware of that to deal with concurrency correctly. You can wrap a
commit: around any chunk of code to ensure you have a write lock for that object like so...
auction commit: [
auction addBid: (Bid
price: 30 dollars
user: self session currentUser) ].
commit: saves the instance within a critical section,
critical: lets you decide when to call
save, in case you want other actions inside the critical section of code to do something more complex than a simple implicit save. When you’re working with multiple distributed systems, like a credit card processor, transactions don’t really cut it anyway so you might do something like save the record, get the authentication, and if successful, update the record again with the new authentication.
auction critical: [
save ] on: Error do: [ :error | auction reopen; save ] ]
Here is another example of the use of a use of the
(CAAuction findAll: [ :each | each needsClosed ]) do:
[ :each |
each critical: [
[ each bids ifEmpty: [ each expireAuction ].
each flushNotifications ]
[ :error |
each abortNotifications ] ] ]
SDActiveRecord for aggregate roots where you need to be able to query for the object, for all other objects just use ordinary Smalltalk objects. You do not need to make every one of your domain objects into ActiveRecords, choosing your model carefully gives you natural transaction boundaries since the commit of a single ActiveRecord and all ordinary objects contained within is atomic and stored in a single file. There are no real transactions so you cannot atomically commit multiple ActiveRecords.
That’s about all there is to using it, there are some more things going on under the hood like crash recovery and startup but if you really want to know how that works, read the code. It is similar to the approaches we presented before. SandstoneDB is available on SqueakSource and is MIT licensed and makes a handy development and prototyping or small application database for Seaside.
The limits or disadvantages of SandstoneDB are that you have to inherit from SDActiveRecord, and it goes against clean domain separation. It makes it harder to reuse your domain code. Now you can define SDActiveRecord as a trait and use this trait in your domain code without being forced to change your inheritance hierarchy. Another disadvantage is that SandstoneDB is designed for small projects that are satisfied with one single image. SandstoneDB neither provides distributed object access (there is always just one image that accesses the data) nor transactional semantics (two concurrent processes could create fatal conflicts in the data structures, unless the developer uses propre concurrency control himself).
OO purists wouldn’t want domain objects to be linked to a persistency framework. You can see this in the design of most OODBs available, it’s considered a sin to make you inherit from a class to obtain persistence. The typical usage pattern is to create a connection to the OODB server which basically presents itself to you as a persistent dictionary of some sort where you put objects into it and then commit any unsaved changes. They will save any object and leave it up to you what your object should look like, intruding as little as possible on your domain, so they say. In Pharo and Squeak, one possible solution explored by SqueakSave (a new framework to save objects in Squeak) is to turn the root class
SDActiveRecord into a trait (trait are compiled-time group of methods) and to apply the trait to your classes. Behind the scenes there’s some voodoo going on where this persistent dictionary tries to figure out what’s actually been changed either by having installed some sort of write barrier that marks objects dirty automatically when they get changed, comparing your objects to a cached copy created when they were originally read, or sometimes even explicitly forcing the programmer to manually mark the object dirty. The point of all of this complexity is to minimize writes to the disk to reduce IO and keep things snappy. This is what we will see next with Magma.