Login

Mapping Seaside Blog to PostgreSQL with Glorp

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

Becomes...

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.

Comments (automatically disabled after 1 year)

Martial 3551 days ago

Hi,

Your tutorial is a pretty good work. It's exactly what I was looking for. But I don't understand the last part. I mean it looks like there is no definition of the database message for the session. I guess it must reach aPostgreSQLPlatform object but I don't know how to do this. I'm blocked at the line 'So far so good...' and after that I am a bit lost. I made a new WASession subclass and I added the following methods but it doesn't work...

Martial

Ramon Leon 3551 days ago

OK, I added the missing methods for session. I left them out because I'm not sure I want to keep one Glorp session per one Seaside session, I have to do a bit of testing before I decide what's best practice there.

Martial 3550 days ago

Thanks! It works now. I agree there must be a better way. I thought about create a BlogDatabase class to manage the session connection because aWASession object is only seen from the BlogView thus I guess it's not a pure MVC architecture. In such a class, I copy the methods you create for the session and so, I can use the message addCondition:labelled used by BlogPost class>>descriptionTitle as you show it in your previous video tutorial. I write this as follows:

addCondition: [:value | (BlogDatabase commit: [BlogDatabase database readOneOf: BlogPost where: [:each | each title = value]]) isNil] labelled: 'Already here';

Finally I notice you forgot to mention the creation of the i-var persistentId in BlogPost and BlogComment and their accessor messages.

Ramon Leon 3550 days ago

Actually, the session is available from anywhere as a singleton, so BlogDatabase database would be the same as saying WACurrentSession value database.

However, as I said, I'm still working on how I'd like to do it. As for the persistentId, I'll be removing that, Glorp supports pseudo variables and doesn't really require it. I'd also implement that rule with a database constraint rather than a query, so I wouldn't use it anyway.

I'm currently busy creating my own version of ActiveRecord that creates all the mappings automatically from the meta data contained within the Magritte descriptions. I have it working and have the blog mapped to PostgreSQL with zero configuration. I'll figure out how I want to handle the session along with this and open source the code shortly after I do some more testing and feel it's stable enough.

Martial 3550 days ago

Oops! I have more to learn about seaside and the session... An ActiveRecord should be far better. If you need help for testing your classes, tell me!

Regards,

Martial

Ramon Leon 3550 days ago

It's sort of like ActiveRecord, except the database is never read, only written, I consider the Smalltalk objects the real schema and the database as totally subservient to them.

Valy 3506 days ago

Hi,

I am trying to get started with Seaside ( I am new to Squeak and Smalltalk ), and I am getting stuck at several points.

One is the access to Postgresql ( which I will need for a new project ). My confusion begins with the versions and VM images, 3.9 for example lists Glorp in the packages list but fails installation ( not supported ), the link for the Squeak port found here http://www.glorp.org/versions.html is dead.

Is there any website/blog with an image that includes the necessary 'components' (Glorp, script.aculo.us, Seaside,etc) ?

Valy

Ramon Leon 3505 days ago

I will see about putting up a ready to run Glorp image tonight.

BradleyWinters 3466 days ago

Nice. I absolutely agree with you. Keep up the nice work. I'll be back for more!

LavernRengerson 3465 days ago

Oh I like that! Nice post. This place is alright...I'll be back for sure.

And 3275 days ago

I wrote a small app with Seaside using Squeak just to get the "feel" of it, but I am new to Smalltalk and there are two things that prevents me to begin using it for "real" projects at my work and it will be very helpful to get some clarifications and/or advice:

  1. Understanding how to use Glorp sessions from Seaside. At this moment I just created a singleton to get the session, is that the right way? I mean I have no idea how multiple requests are handled by Seaside and if that approach works for multiple queries from different users,etc.

  2. Deployment My web applications are used internally and for some clients, there is no need for huge scaling, a few dozen concurrent users tops. Will only one Squeak instance be sufficient? If not, should I follow the posts made about the deployment architecture used in DabbleDB and used that kind of setup with Apache,etc?

Ramon Leon 3275 days ago

See my MagritteGlorp package on SqueakSource, it has the necessary code to integrate Seaside with Glorp including a custom seaside session class, a connection pool, and a few other things. You don't actually need to use Magritte, you can write your Glorp descriptor normally and use the Seaside Glorp integration.

For that small a load, a single squeak image will probably work just fine.

And 3275 days ago

Thanks! I will study that code and try to 'get it'.

I downloaded your image...wow! What a difference from the default Squeak image, fantastic!

Ramon Leon 3275 days ago

I have a much better one now, a great new UI package was released that really cleans Squeak up. I'll update my image here soon so anyone can download it. Email me or leave a comment here if you have any questions about the package.

Mikey 3225 days ago

Yeah, this is a very very cool blog. ;-) I just added you to my favorites.

Thanx, Mikey

Carlos 3124 days ago

Ramon, very nice post. I found a demo in the Cincom site that illustrates this article in VW steb by step. Seems that there is some UI code, maybe built by UIPainter. I am interested in how a glorp-based model i editable-viewable with a classic GUI. Regards, Carlos

Ramon Leon 3124 days ago

I don't really know anything about Visual Works, I'm just a Squeaker, sorry.

about me|good books|popular posts|atom|rss