In a previous post I created a simple blog containing two business objects, BlogPost and BlogComment. I implemented persistence for that blog in the simplest manner possible, I put an OrderedCollection of BlogPosts in an accessor called repository on the BlogPost class...
BlogPost class>>repository ^repository ifNil: [repository := OrderedCollection new]
Allowing the class itself to serve as the database for all its instances. This is great for development speed, and gives you the ability to play and try out many different things until your object model settles down and you figure out what your model is really going to look like. Using the image as an object database allows you to remain nimble as you learn more about the problem and change your mind as you gain new insight into the domain model. Put off transactional persistence as long as you can, it's likely you can build almost your entire program without actually leaving the image or thinking about a database.
Let's assume the we're at that point now, and we're going to map BlogPost and BlogComment into a real relational database as if this were a production app. I'm going to use PostgreSQL and Glorp, my first time using either, and see what it takes. I'm not claiming anything I do here is best practice, only sharing my experiences as I learn.
For the record, Alan Knight, the author of Glorp, has a working implementation of ActiveRecord, somewhat like Ruby on Rails, that would allow me to avoid all this meta-data, but as far as I know, it hasn't been ported to Squeak yet, and is still in Alpha; I hope he's done soon.
Until ActiveRecord is available, I'll have to write the meta-data the old fashioned way. After reading the Glorp Tutorial, I see I need to subclass DescriptorSystem and create a class containing all the meta data for classes, tables, and the mappings between them. I call it SBGlorpDescriptions.
It seems I need to override two methods to tell Glorp about the tables and classes...
allTableNames ^#(POST COMMENT) constructAllClasses ^(super constructAllClasses) add: BlogPost; add: BlogComment; yourself
Then I need to write methods for the table meta-data. Glorp will create the actual schema in PostgreSQL for me from this metadata, as well as use it to map between tables and classes.
tableForPOST: aTable (aTable createFieldNamed: #postId type: platform sequence) bePrimaryKey. aTable createFieldNamed: #title type: (platform varchar: 100); createFieldNamed: #body type: (platform text) tableForCOMMENT: aTable | postId | (aTable createFieldNamed: #commentId type: platform sequence) bePrimaryKey. postId := aTable createFieldNamed: #postId type: platform int4. aTable createFieldNamed: #name type: (platform varchar: 100); createFieldNamed: #comment type: platform text; addForeignKeyFrom: postId to: ((self tableNamed: #POST) fieldNamed: 'postId')
I also need to write methods for the class model which gives me a chance to control how Glorp loads the classes. For example, I may want to force Glorp to use accessors rather than direct instance variable access. Here I simply tell Glorp that a Post has a collection of Comments.
classModelForPost: aModel aModel newAttributeNamed: #persistentId; newAttributeNamed: #title; newAttributeNamed: #body; newAttributeNamed: #comments collectionOf: BlogComment classModelForComment: aModel aModel newAttributeNamed: #persistentId; newAttributeNamed: #name; newAttributeNamed: #comment
Now Glorp has a meta-data model for both classes and tables. Now, given the table meta-data and the class meta-data, I create a map between them for each class on each class...
BlogPost class>>glorpSetupDescriptor: aDescriptor forSystem: aDescriptorSystem | table | table := aDescriptorSystem tableNamed: #POST. aDescriptor table: table. (aDescriptor newMapping: DirectMapping) from: #persistentId to: (table fieldNamed: #postId). (aDescriptor newMapping: DirectMapping) from: #title to: (table fieldNamed: #title). (aDescriptor newMapping: DirectMapping) from: #body to: (table fieldNamed: #body). (aDescriptor newMapping: OneToManyMapping) attributeName: #comments; referenceClass: BlogComment; collectionType: OrderedCollection BlogComment class>>glorpSetupDescriptor: aDescriptor forSystem: aDescriptorSystem | table | table := aDescriptorSystem tableNamed: #COMMENT. aDescriptor table: table; addMapping: (DirectMapping from: #persistentId to: (table fieldNamed: #commentId)); addMapping: (DirectMapping from: #name to: (table fieldNamed: #name)); addMapping: (DirectMapping from: #comment to: (table fieldNamed: #comment))
I'm not using the real power of Glorp here, because my models mostly match, however, were I programming against an existing legacy schema, I might start to appreciate all this meta-data a bit more, knowing I could map any shape class to any shape table. This is where Glorp truly shines over Rail's ActiveRecord, which is an extremely simple and not so flexible object mapping system. Everything I'm doing here, Rails could do easily, but Glorp is much more powerful and can be used against legacy schemas that look nothing like the class model.
Rails does one thing, one way, very well, with inferred meta-data and virtually no configuration. Glorp can do anything, any way you like it, but requires explicit meta-data, with a bit of configuration. Alan's about to give us the best of both worlds, by inferring the basic meta-data like Rails does, but allowing you to mix and match it with custom meta-data for more complex mappings, I can't wait, and I'm glad Rails has pushed Alan in this direction. Glorp will be much easier to use for greenfield applications if it infers the meta-data with reasonable defaults, and Rails has proven how popular this approach is with developers.
OK, now that all the meta-data and mappings are created, I need to have Glorp create the schema for me. I fire up a Workspace and create a Glorp session using my new SBGlorpDescriptions class...
login := (Login new) database: PostgreSQLPlatform new; username: 'xxxxxx'; password: 'xxxxxx'; connectString: '127.0.0.1_seasideBlog'. accessor := DatabaseAccessor forLogin: login. accessor login. session := GlorpSession new. session system: (SBGlorpDescriptions forPlatform: login database). session accessor: accessor.
Then I tell Glorp to create my schema...
session inTransactionDo: [session system allTables do: [:each | accessor createTable: each ifError: [:error | error inspect]]].
So far so good, everything works, time to change the blog and query through Glorp. On my Seaside session, I created a few delegation methods to pass through to the Glorp session which it contains...
database ^database ifNil: [database := self buildDbSession] buildDbSession | login accessor | login := (Login new) database: PostgreSQLPlatform new; username: 'xxx'; password: 'xxx'; connectString: 'localhost_seasideBlog'. accessor := DatabaseAccessor forLogin: login. accessor login. ^(GlorpSession new) system: (SBGlorpDescriptions forPlatform: login database); accessor: accessor; yourself unregistered super unregistered. self database accessor logout commit: aBlock ^self database inUnitOfWorkDo: aBlock execute: aQuery ^self database execute: aQuery register: anObject ^self database register: anObject
And then make a few changes to the main blog component...
blogPosts ^BlogPost repository reversed
blogPosts ^self session execute: ((SimpleQuery returningManyOf: BlogPost limit: 10) orderBy: [:each | each persistentId descending]; yourself)
And I modify #newPost and #addCommentTo: to look like so...
newPost | post | post := self call: ((BlogPost new asComponent) addValidatedForm; yourself). post ifNotNil: [self session commit: [self session register: post]] addCommentTo: aPost | comment | comment := self call: ((BlogComment new asComponent) addValidatedForm; yourself). comment ifNotNil: [self session commit: [self session register: aPost. aPost addComment: comment]]
And that's it, the sample blog now works against PostgreSQL. In all honesty, it was a bit of work, more than I expected, but given Glorp's capabilities, I understand the need for so much meta-data. I'll sure be glad when Alan finishes his ActiveRecord implementation because most of the time, that's all I need.
In the mean time, I'll have to write up a quick code generator to generate most of this meta-data for me directly from the classes using reflection. I've got a bigger project coming up where I plan to use Glorp, and there's no way I'm writing all this by hand again.