Seaside and GOODS

David Shaffer
Shaffer Consulting

GOODS

GOODS ("Generic Object-Oriented Database System") is a language-neutral object database. The best place to read about GOODS is on the GOODS home page. Actually to just get started you don't need to know much about GOODS. Download, compile and install GOODS in such a way that the goodsrv executable is in your path or you know the explicit path to it. I'll leave it to you to figure out how to do this on the platform of your choice (the GOODS distribution includes installation documentation).

For this tutorial it would be sufficient to have a database server running with a single storage manager. To accomplish this, create a directory called "TutorialDB" and in that directory create a file called "TutorialDB.cfg" with the following contents:

1
0:localhost:2000
This tells the GOODS server monitor to start a single storage manager on port 2000 of localhost. We will use this information (host name and port number) when we contact this server from squeak. To start the database server in Linux all one needs to do is to change to the TutorialDB directory and type "goodsrv TutorialDB". I would imagine that the process of starting the server monitor is similar on other platforms. The goodsrv server monitor will read our .cfg file and start the storage manager. If no other configuration options are specified, goodsrv will sit and wait for administrative commands from the user (type "help" to get a list).

Using GOODS from Squeak

Avi Bryant has written a Squeak GOODS client. The wiki page provides examples of connecting to the database and storing and retrieving objects. Spend a few minutes reading through those pages and playing with the examples. In the remainder of this section we will simply set the stage for using GOODS in this tutorial.

Every GOODS database has a single root object. Every object stored in the database is reachable (directly or indirectly) from that root object. Normally the root object of the database should be a Dictionary-like object. Make the root of your database a Dictionary by evaluating this code:

db := KKDatabase onHost: 'localhost' port: 2000.
db root: Dictionary new.
db commit.
db logout.

Typical database applications have several collections of objects reachable from the database root Dictionary. Examples might include a list of guestbook entries, a set of schedule entries etc. The keys in the root Dictionary act as paths to the actual root objects in your database. For example, I might set up an OrderedCollection to hold guestbook entries like this:

db := KKDatabase onHost: 'localhost' port: 2000.
db root at: 'guestbookEntries' put: OrderedCollection new.
db commit.
db logout.
This is worthy of abstraction in the form of a class. I use class-side methods to manage the database entries. Instances of subclasses of ListRootPersistent are normally objects reachable from an OrderedCollection which is referenced from our root Dictionary. Here are the ListRootPersistent class and the RSVPEntry example. File these in to your Smalltalk image and open browsers on the class-side methods of ListRootPersistent and its subclass RSVPEntry. First there is the one-time initialization message ListRootPersistent>>initializeInDatabase: which creates the list as we did in our code fragment above. It relies on subclasses overriding listName to return the String name of root Dictionary entry. So, for our RSVP application we need to evaluate:
db := KKDatabase onHost: 'localhost' port: 2000.
RSVPEntry initializeInDatabase: db.
db commit.
db logout.
There, now we have a place to store the entries. Next look at the entries protocol in ListRootPersistent. You can clearly see how to add and remove entries as well as directly access the collection. Here is a sample code fragment:
db := KKDatabase onHost: 'localhost' port: 2000.
entry := RSVPEntry new
	name: 'Bob Smith';
	numberAttending: '3';
	location: 'The wedding hotel room 179';
	emailAddress: 'me@nowhere.net';
	yourself.
RSVPEntry addEntry: entry in: db.
db commit.
db logout.

Configuration

In a previous section I provided code for a configuration class that works well with GOODS. We will also use the following configuration (THIS NEEDS A MORE GENERIC NAME):
WASystemConfiguration subclass: #GuestbookConfiguration
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Guestbook'

attributes
	^ Array
		with: (WAStringAttribute key: #email group: #Guestbook)
		with: (WAStringAttribute key: #site group: #Guestbook)

The RSVP application

Let's create this simple RSVP application. The basic idea is that you would use this application for people to RSVP for a party or wedding, for example. From the main party/wedding page you would link to this application, the person would enter their RSVP and you would redirect them back to the wedding site. The flow of the main application is roughly:
  1. Create a new RSVPEntry
  2. Open an editor on that entry
  3. Save the entry in GOODS
  4. Email a confirmation to the configured email address
  5. Inform the user that everything went fine
  6. Redirect to the configured site
We will use a Task to capture this sequence of operations:
WATask subclass: #RSVPTask
	instanceVariableNames: 'rsvp'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Guestbook'

go
	| editor |
	editor := RSVPEditor new.
	rsvp := RSVPEntry new.
	editor model: rsvp.
	(self call: editor)
		ifTrue: [self save]
		ifFalse: [self redirectToSite]
	 

goodsHost
	^self session application preferenceAt: #goodsHost 

goodsPortNumber
	^self session application preferenceAt: #goodsPortNumber 

informUserOfError
	self inform: 'Something went wrong delivering your RSVP.' 

informUserOfSuccess
	self inform: 'Your RSVP has been delivered.' 

redirectToSite
	^self session redirectTo: (self session application preferenceAt: #site) 

save
	
	[self saveToDatabase.
	self sendEmail.
	self informUserOfSuccess.
	self redirectToSite] 
			on: Error
			do: 
				[:ex | 
				self informUserOfError.
				self redirectToSite] 

saveToDatabase
	| db |
	db := KKDatabase onHost: self goodsHost port: self goodsPortNumber asNumber.
	RSVPEntry addEntry: rsvp in: db.
	db commit 

sendEmail
	rsvp mailNotificationTo: (self session application preferenceAt: #email)

"CLASS SIDE!"
canBeRoot
	^ true
And now the editor class:
WAEditDialog subclass: #RSVPEditor
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Guestbook'

rows
	^#(name numberAttending emailAddress location comment) 


buttons
	^ #(sendRSVP cancel) 

labelForSelector: sel
	sel = #location ifTrue: [^'Where are you staying in case we need to contact you?'].
	^super labelForSelector: sel 

renderCommentOn: html
	html attributeAt: 'rows' put: 5.
	html textAreaOn: #comment of: self model 

renderLocationOn: html
	html attributeAt: 'rows' put: 5.
	html textAreaOn: #location of: self model 

sendRSVP
	^self save 

validate
	self model isComplete ifFalse: [self validationError: 'Entry is not complete'] 

initialize
	super initialize.
	self addMessage: 'RSVP'.
To avoid obscuring the code above I left out the style method which can be found here. Use the configuration application to register RSVPTask as the root of an application. Be sure to add GuestBookConfiguration and GoodsAppConfiguration to its configuration list and give the e-mail address and site values. This can be done in code by evaluating the following:
app := RSVPTask registerAsApplication: 'rsvp'.
app configuration addAncestor: GuestbookConfiguration localConfiguration.
app configuration addAncestor: GoodsAppConfiguration localConfiguration.
app preferenceAt: #goodsHost put: 'localhost'.
app preferenceAt: #goodsPortNumber put: 2000.
app preferenceAt: #email put: 'the-admin@somesite.net'.
app preferenceAt: #site put: 'http://www.the-original-site.com'.

Listing the RSVP's ????????????????????????BROKEN: NEED TO SEPARATE FROM GOODSSESSION

Email can be fairly unreliable and the application above doesn't try very hard to deliver the mail. So, the person who is keeping track of the RSVP's might want a simple application to list them:
WAGridDialog subclass: #RSVPList
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Guestbook'

buttons
	^#(refresh) 

columns
	^ #(#name #emailAddress #numberAttending #location #comment ) 

items
	self session database refresh.
	^RSVPEntry allEntriesIn: self session database 

refresh
	self ok

"CLASS SIDE"
canBeRoot
	^ true
Register this as a new application and add the GoodsAppConfiguration:
app := RSVPList registerAsApplication: 'rsvpList'.
app configuration addAncestor: GoodsAppConfiguration localConfiguration.
app preferenceAt: #goodsHost put: 'localhost'.
app preferenceAt: #goodsPortNumber put: 2000
At this point you should get a fairly ugly list of the RSVP's in the database. Still, it works well enough for now.

Notes and things to consider

Here are some miscellaneous points that didn't seem to fit directly in the material above.
  1. Changing the shape or field types of a class presents challenges when using an Object-Oriented database. GOODS helps a little by keeping tracke of i-var names and so reordering and removing instance variable is handled directly. Adding an i-var to a class will cause instances persisted using the old version of the class to have a nil value for that i-var. More complicated changes (consider what would happen if we decided that the #numberAttending field should really be a number) require either bulk instance migration or code in the class to handle conversion lazily (on load).
  2. Not all commit conflicts are caught by the current GOODS client implementation in Squeak. ELABORATE
  3. Strings are considered immutable by the Squeak GOODS client so mutating a persistent String will lead to odd behaviors.
  4. Performance of Dictionary and OrderedCollection will be poor, especially as these collections get large. Both of them are array based and GOODS will send the entire array to the client when these collections are accessed. Fortunately we all remember our undergraduate Data Structures course, right? Anyway, if you want a Dictionary-like class that is typically faster (in GOODS) use Avi Bryant's BTree package (on SqueakMap). Speed of SequenceableCollection's depend heavily on the operations being performed. Often a linked list works quite well but probably not for applications that frequently iterate over the entire collection.
  5. Be careful with assumptions about object identity when dealing with persistent objects. Suppose you use objects whose implementation of hash and == are inherited from Object as keys in a persistent Dictionary. When the Dictionary is loaded from the database the hash codes of these objects will probably not be the same as when it was stored causing it to be corrupted. Avi has included KKIdentityBTree and KKIdentitySet to alleviate this problem.

C. David Shaffer
Last modified: Mon Sep 19 09:08:31 EDT 2005