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:2000This 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).
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.
WASystemConfiguration subclass: #GuestbookConfiguration instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Guestbook' attributes ^ Array with: (WAStringAttribute key: #email group: #Guestbook) with: (WAStringAttribute key: #site group: #Guestbook)
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 ^ trueAnd 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'.
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 ^ trueRegister 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: 2000At this point you should get a fairly ugly list of the RSVP's in the database. Still, it works well enough for now.
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).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.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.