[Grok-dev] thoughts while writing a tutorial
Brandon Craig Rhodes
brandon at rhodesmill.org
Fri Aug 17 08:27:15 EDT 2007
Wichert Akkerman <wichert at wiggy.net> writes:
> Previously Brandon Craig Rhodes wrote:
>> Two weeks ago I promised a tutorial on using SQLAlchemy from
>> Grok...
>
> I am actually working on the very same thing as well at the
> moment. Can you give me a pointer to your example code?
I have nothing on the web yet. Since it looks like a week or two of
learning before I have an actual working tutorial, let me quickly
outline my approach for you, Wichert.
1. Define Your Schema
I put my schema in a "schema.py" module, to keep things clean; but
as long as you put some information into ZAlchemy's "metadata",
you've done the job you need to. If you define your tables
statically in your code, things are easy:
from sqlalchemy import (
Table, Column,
Integer, String, DateTime,
ForeignKey,
)
import z3c.zalchemy
metadata = z3c.zalchemy.metadata()
services = Table(
'services', metadata,
Column('tag', String(), unique=True, nullable=False),
Column('name', String(), unique=True, nullable=False),
Column('fullname', String(), unique=True, nullable=False),
Column('status', String(), nullable=False, default='Running'),
)
... and so forth, through as many tables as you like. Though I at
first wanted SQLAlchemy to use its powers of introspection to
determine what tables our database system has - because describing
an existing database in a new programming language like this
duplicates information, and requires changes to be made two places
the next time they change a table - I realized that being explicit
about the database structure has two important advantages:
* First, it means that testing becomes easy, because your test
suite can import the "schema" module and then ask each of the
described tables to create itself in a test database. This
means you can do unit tests in the absence of an external
database! By contrast, if we relied upon introspection to fill
in the schema, you could only test by mocking up the tables
separately.
* Second, since your code is now explicit about how the database
should work, it will fail cleanly (by refusing, with a clear
message, to run at all!) on the day that one of the tables
changes without your knowing it was going to; or when you try
running it against a wrong or old version of your application
database.
If you, despite these reasons, still want to introspect the schema
from the database, then you will need to briefly create an open
engine to the database at schema-instantiation time, and your code
will look something like (I'm doing this from memory, so the
keyword args might be wrong):
engine = sa.create_engine('postgres://localhost/mydatabase')
services = Table(
'services', metadata, autoload=True, with=engine,
)
del engine
There is probably a more clever way to create the engine by
instantiating the utility we will create (see below); but the above
does work.
2. Define Your ORM
If your application simply wants to query tables like those defined
above, then skip this section. But if you want to use the
SQLAlchemy ORM, that lets you "hook up" Python objects to tables so
that each object instance represents a particular table row, then I
would create a module called "orm.py" and do something like:
import sqlalchemy
import your_app.schema as schema
# Create a class to "hook up" to SQLAlchemy.
class Service(object):
pass
# And, hook it up.
sqlalchemy.mapper(Service, schema.services)
Of course you might want to create fancier objects than this tiny
"Service" class; you might even have big objects defined in other
files that you "import" here to use "mapper" on. This would lead
to the interesting property that you could test the behavior of
your ORM object methods in the absence of any database - because if
a unit test or doctest chose not to import "orm.py", then the
objects would just stand "on their own" and you could create them
and call them and it would simply manipulate its instance variables
as local Python values. But an integration tester could actually
import "orm", and then references to the object's attributes would
read and write to the database.
3. Connecting To Your Database
Finally, you need to provide a utility that knows how to connect to
your database. Create a module like "database.py" or something
that says (and the ZAlchemy module documentation has more examples
of how this can work):
import sqlalchemy as sa
from z3c.zalchemy.interfaces import IAlchemyEngineUtility
from z3c.zalchemy.datamanager import AlchemyEngineUtility
from zope.component import provideUtility
engineUtility = AlchemyEngineUtility(
'my_database',
'postgres://localhost/my_database',
echo=False,
)
provideUtility(engineUtility, IAlchemyEngineUtility)
With this present in your application - and also, when any test
suite chooses to import or grok this module - then code that uses
the database tables in "schema.py" or the objects in "orm.py" will
know how to talk to your database.
4. Finally, How To Get A Session
All you now need to have working code is a database session!
Every time you need to access or manipulate objects or tables, you
will need to create a session; the SQLAlchemy ORM manual is full of
examples of using sessions to get objects from the database and
save them back. To do this in your Grok app, do:
session = z3c.zalchemy.datamanager.getSession()
You might even want a little "db.py" module that provides this call
for you more conveniently.
According to the ZAlchemy documentation, the changes made to the
database with the session you grab will only be committed when the
current Zope 3 transaction commits, and will be aborted if the
transaction is cancelled. I myself have not tested this yet, and I
am sure what events, exactly, would cause a commit or abort; can
this happen automatically, for example, if the user cancels the
browser request while it's running? Zope transactions are one of
the things I will have to read up on as I expand the above into a
full tutorial, so that I can explain them better.
5. Testing
As noted above, there are several tricks to make testing easier -
like having a separate "orm.py" module that connects all of your
objects to the database, so that the objects themselves can be
tested without SQLAlchemy interfering at all by sometimes not
importing "orm". But some of your unit tests will need a database,
and I found it was easiest to provide this through two steps.
Briefly:
- Provide a convenience module that creates an in-RAM database and
creates all your tables there.
- Call this module from your unit tests or doctests.
The question (as you might have seen on this mailing list recently)
is where to put the module! Giving it a "normal" name would mean
it would be grokked when you tried to run your whole application,
so we need its name to start with "test"; but this means that the
"./bin/test" script will try to run it as a test suite and complain
if it lacks a test_suite function! So I compromised and created a
"test_testdb.py" module (but asked whether Grok could learn to
ignore "test" directories completely):
from zope.component import provideUtility
from z3c.zalchemy.interfaces import IAlchemyEngineUtility
from z3c.zalchemy.datamanager import AlchemyEngineUtility
# Create a database that lives only in RAM.
engineUtility = AlchemyEngineUtility(
'database',
'sqlite:///:memory:',
echo=False,
)
provideUtility(engineUtility, IAlchemyEngineUtility)
# Second, create all of the tables defined in our schema, so
# that they are available for testing.
from myproject.schema import *
engine = engineUtility.getEngine()
for table in [ services ]:
table.create(bind = engine)
# Finally, since the Zope "testrunner" will discover us cowering
# here and think from our name that we are a test, we provide an
# empty test suite to prevent its throwing an error.
import unittest
def test_suite():
return unittest.TestSuite()
And now, with this module to make things convenient, something like
a doctest can look like:
>>> import grok
>>> grok.grok('myproject.tests.test_testdb')
>>> grok.grok('myproject.schema')
>>> grok.grok('myproject.orm')
>>> from myproject.orm import Service
Here we go!
>>> session = z3c.zalchemy.datamanager.getSession()
>>> service1 = Service()
>>> service1.tag = 'email'
>>> service1.name = 'Email'
>>> service1.fullname = 'Central Campus Email'
>>> service1.status = 'running'
>>> session.flush()
>>> print service1.tag
'email'
The important thing when testing this way is to never, ever, say
"import transaction; transaction.commit()", because - for reasons
that are not clear to me - the ZAlchemy code closes the database
connection once "commit()" has been called, which destroys the
in-memory database we created by passing ":memory:" to sqlite!
A normal database, of course, exists even when you're not connected
to it, so calling "transaction.commit()" is fine, because your data
will still be there when your new session re-connections. But when
using a ":memory:" database for fast and footprint-less testing
(you could, of course, use a tmpfile to avoid this), limit yourself
to "session.flush()" to make data appear.
There!
That's about everything I've learned so far. And having typed all
this out will make writing the tutorial next week easier. Let me
know, Wichert, whether it answered any questions, or whether you are
trying to do something it does not address at all; maybe there are
additional approaches I should describe in the finished tutorial.
--
Brandon Craig Rhodes brandon at rhodesmill.org http://rhodesmill.org/brandon
More information about the Grok-dev
mailing list