[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