[Grok-dev] grok and the ORM of your choice
Brandon Craig Rhodes
brandon at rhodesmill.org
Fri Jul 20 09:26:35 EDT 2007
Tim, after two or three days of playing with Grok - and very much
liking what I'm seeing - I have some database objects displaying in a
web page. Since I'm very new at this, any of the steps below could be
the wrong approach; hopefully other list readers will point out better
techniques (and I will run off and rewrite using them!). I'll happily
develop what follows into a tutorial once I've learned enough.
My tiny application looks something like this:
1) I am using SQL Alchemy to access the database. This may skew
things a bit, since SA's approach is to "install itself" on classes
that the programmer creates as normal Python classes. So to
represent a Person, I create a class:
class Person(grok.Model):
"""A person from inside our database."""
@property
def full_name(self):
"""Build a full name from pieces in the database."""
if self.first:
return '%s, %s' % (self.last, self.first)
else:
return self.last
As you can see, I can have whatever properties or functions already
there that I want, as long as they don't conflict with the column
names that SA will want to install. Then I hook the table up to SA:
import sqlalchemy as sa
engine = sa.create_engine('postgres://localhost/mage')
metadata = sa.DynamicMetaData('mymeta', threadlocal=False)
metadata.connect(engine)
sa.mapper(Person, sa.Table('people', metadata, autoload=True))
and I am up and running with a working Person object, whose
instances will now have an attribute for each column in the
database (like a Person has a .name and so forth).
2) I want people to appear under URLs like /person/932113, where the
number is an integer called their "GTID". Accounts appear under
URLs like /account/br32 where "br32" is their username. To do
this, I create two "layers" of traversal. First, I have a base
traversal class that knows the names "person" and "account" are
special, and hands those off to class instances that are ready to
handle the second URL component (we'll define "TableTraverser" in a
minute):
dynamic_urls = {
'person': TableTraverser(Person, 'gtid'),
'account': TableTraverser(Account, 'username'),
}
class BaseTraverser(grok.Traverser):
grok.context(MyApplication)
def traverse(self, name):
if name in dynamic_urls:
return dynamic_urls[name]
This "BaseTraverser" sits on top of the application itself, named
"MyApplication", and when given a "name" that someone is trying to
visit under the URL of my application, it looks to see if it's in
the "dynamic_urls" dictionary. If so, then it returns the
corresponding "TableTraverser" (which we'll see in a moment) as the
object from which to make the next hop down the URL.
The TableTraverser itself is a bit more complicated, because it has
to dynamically look up the next path name component as a key in a
table in order to find the object reached through the URL:
class TableTraverser(grok.Model):
def __init__(self, table, attr):
self.table = table
self.attr = attr
def traverse(self, name):
session = sa.create_session()
query = session.query(self.table)
search_results = query.select_by(** { self.attr: name })
if search_results:
obj = search_results[0]
obj._remember_my_session_ = session
return obj
But all in all, I think this TableTraverser is still a fairly
simple class. Look at what it does: if we try to access
"person/901", then the traverser will be run who was told to use
the object class "Person" and the column "gtid". So it will
dutifully fire up a SQL Alchemy session, look for the Person table
row for which "gtid = '901'", and return it if found.
When a traverser returns an object, the traversal was successful
and the path (or at least the part traversed so far) results in
the user "landing" on the object that traverse() returns.
When a traverser returns None - which is what the function above
will do by default if it does not get any search results back -
then the traversal has failed; which is exactly what we want in
this case if the GTID given does not exist in the database!
There is only one oddity here, and it probably indicates I don't
know enough about Grok/Zope 3 session management.
The oddity is having to save the SQL Alchemy "session" as an
attribute of the Person or Account object itself that we are
handing back: I place it on a cute instance variable named
_remember_my_session_. The reason is that, before I did this, any
attempt to visit other objects in the database failed with the
statement that I had no active session; apparently, SQL Alchemy
allows you to close a session but all the objects will remember
what they last looked like - they'll just lose the ability to grab
new information from the database.
The symptom of this failure was that I'd try to visit, say, a
Person's "Account" objects to display them on a Person-related web
page, and when I tried to look up their Accounts, SQL Alchemy would
return an error about my having no active session.
So by making the session an instance variable of the Person, the
session is guaranteed to "live as long as the Person object does",
and won't drop out of existence until my web page has finished
rendering.
3) Once the Person or Account object has been reached by the URL,
there need to exist Views to render them for my customers using web
browsers. Right now I just have an index page for each kind of
object, to which I give the name "index". That name seems to be
special inside of Zope 3; while one can see an object by adding
"index" to the URL explicitly like ".../person/900010011/index", it
also appears to be the view that is used if no view name is added
to the end of the URL at all like "../person/900010011". The two
views are very simple right now:
class PersonIndex(grok.View):
grok.name('index')
grok.context(Person)
class AccountIndex(grok.View):
grok.name('index')
grok.context(Account)
All they have to do is use grok.context() to state what kind of
object they are designed to view, and grok.name() to indicate what
they are called at the end of a URL, and everything "Just Works".
Which, I suppose, is the magic of Grok.
Corresponding to each of the above classes is a template file
inside of my "app_templates" directory:
$ ls src/iam/app_templates
accountindex.pt personindex.pt
My Person index is by now quite complicated, but an earlier version
of it will give you an idea of how things work:
<html>
<body>
Name: <b tal:content="context/name">Name</b><br>
GTID: <b tal:content="context/gtid">GTID</b>
</body>
</html>
This displays the attributes "person.name" and "person.gtid" of my
Person object on the web page.
When you are writing a template like the one above, there are
several special objects that you can access when writing TAL
template statements like the two you see above in my example:
context - this is the object to which the user traversed using
their URL. So if their URL is ".../person/901", then the
Person object with that GTID is the "context" of the web page
rendering, and its attributes can be accessed (like the two
above) by "context/attribute-name".
view - this gives you the view object itself; in my case, if you
went to a person's URL, this would be the PersonView that was
created when you accessed ".../index" or let "index" be
chosen by default by not specifying a view.
There are a few other objects to which one has access, but I have
not had the chance to use them yet.
4) This brings us to a very important point! Once I had basic Person
information displaying, like in the template above, I wanted to
access more interesting information - things that required adapting
the Person object! Automatic object adaption is why I started
using Zope 3, because I think that it moves beyond MVC and OO in a
wonderful direction.
For simplicity I'll make up a simple example that's not in my
actual application. Let's imagine that, instead of putting the
full_name property on the Person class itself like I did way up
above, I wanted to create an IFullyNamed interface for all sorts of
objects for which interesting full names can be constructed. (Why
would I do that? Imagine that I'm using someone else's library of
objects that already defines Person and Account, and I don't want
to muck with his already-working code.) I might create an
interface:
from zope.schema import TextLine
class IFullyNamed(Interface):
"""Compute a pretty 'full name' from an object's attributes."""
full_name = TextLine(title=u"Full Name")
and then, for the Person, create an adapter that lets a Person
object behave this way:
class FullyNamedPerson(grok.Adapter):
"""Concoct an attractive Full Name for a Person object."""
grok.context(Person)
grok.provides(IFullyNamed)
@property
def full_name(self):
"""Build a full name from pieces in the database."""
if self.context.first:
return '%s, %s' % (self.context.last, self.context.first)
else:
return self.last
Note that the "full_name()" function looks exactly like the one
from my first example, except now it's running on an adapter rather
than on the Person object itself, so it has to say "self.context"
everywhere that before it would have said "self" to talk about the
Person.
Okay, so now we have an example Interface and an Adapter, and we
return to my Big Question: where to make adaption happen. My first
thought was that, since TAL lets me say "context/gtid" for a
person's GTID, I ought to be able to say something very close to
"context/IFullyNamed/full_name" in order to adapt the object
on-the-fly while rendering it for the web. But thanks to excellent
help from the #grok IRC channel on FreeNode, I now realize that I
was wanting to do the wrong thing: templates are not supposed to be
that complicated; they are not supposed to try adapting objects
right in the text of the template!
Instead, return to the PersonIndex object I created way up above -
the grok.View that powers the "index" page to begin with. Notice
how very sparse and bare it is? Well, it's ultimately not going to
stay like that, because it turns out that the View is where one
places all of the mechanics for producing adaption and the other
data synthesis that the template is going to need. In this
example, we could rewrite the view to look something like this:
class PersonIndex(grok.View):
grok.name('index')
grok.context(Person)
@property
def fully_named(self):
return IFullyNamed(self.context)
and then, in the TAL expression, simply say something like
"view/fully_named/full_name" in order to cause the adaptation.
Thus, over time, a View can become full of properties and functions
used to generate all sorts of information the template will need;
and this means that instead of TAL having to become a whole
separate meta-language for writing Python, TAL can stay simple and
we can write ordinary Python in Views to get things done.
5) And that's where things stand so far; I can reach objects using
URLs and display them interestingly, either by pulling their
attributes themselves for display, or forcing adaption or other
interesting behaviors through the View. All of the objects live in
the database, without any ugly residue left on the local disk.
I hesistate to bring up a few real issues I still have outstanding,
since the Grok developers themselves may well have stopped reading
by now; I can see their eyes glazing over as they had to read my
description of how one uses views!
But in case people are still reading, my open issues are:
- I am not yet totally free of the ZopeDB, because one last object,
the Application object itself, needs to be instantiated there!
My end goal is to have no ZopeDB, because the last thing I need
is another database to restore if I have to rebuild the app.
But I have not yet figured out how to run a Grok application
without any ZopeDB whatsoever.
- While users can access my Person and Account objects through the
web, I will sometimes want to access them through scripts. While
the web is great for users clicking on single actions, I'm often
called upon to grant new email aliases to entire departments of
several hundred people at once. I want to be able to write
normal Python scripts where I import my model and act upon it.
But how does one use Grok objects "outside" of a running
instance? I am mightily encouraged by PvW's recent posting
regarding "Death to Instances" involving Zope itself:
http://www.z3lab.org/sections/blogs/philipp-weitershausen/2007_07_07_death-to-instances
But I'm not yet savvy enough to know how this applies to Grok;
how do I write a script that imports and uses my Grok Models?
- I am not sure whether keeping the SQL Alchemy session alive with
a reference from the "context" object is the best way to do
things, or whether there is some Request object in the background
that it would be better to attach it to.
Thanks to anyone who can help with these questions (or with terrible
error I've made in my description above!). Grok is magnificent!
--
Brandon Craig Rhodes brandon at rhodesmill.org http://rhodesmill.org/brandon
More information about the Grok-dev
mailing list