7: RDBMS Root Factories¶
Using SQLAlchemy to provide a persistent root resource via a resource factory.
Background¶
In 6: Storing Resources In ZODB we used a Python object database, the ZODB, for storing our resource tree information. The ZODB is quite helpful at keeping a graph structure that we can use for traversal's "location awareness".
Relational databases, though, aren't hierarchical. We can, however, use SQLAlchemy's adjacency list relationship to provide a tree-like structure. We will do this in the next two steps.
In the first step, we get the basics in place: SQLAlchemy, a SQLite table, transaction-awareness, and a root factory that gives us a context. We will use 2: Basic Traversal With Site Roots as a starting point.
Note
This step presumes you are familiar with the material in 19: Databases Using SQLAlchemy.
Note
Traversal's usage of SQLAlchemy's adjacency list relationship and polymorphic table inheritance came from Kotti, a Pyramid-based CMS inspired by Plone. Daniel Nouri has advanced the ideas of first-class traversal in SQL databases with a variety of techniques and ideas. Kotti is certainly the place to look for the most modern approach to traversal hierarchies in SQL.
Goals¶
- Introduce SQLAlchemy and SQLite into the project, including transaction awareness.
- Provide a root object that is stored in the RDBMS and use that as our context.
Steps¶
We are going to use the siteroot step as our starting point:
$ cd ..; cp -r siteroot sqlroot; cd sqlroot
Introduce some new dependencies and a console script in
sqlroot/setup.py
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
from setuptools import setup requires = [ 'pyramid', 'pyramid_jinja2', 'pyramid_tm', 'sqlalchemy', 'zope.sqlalchemy', 'pyramid_debugtoolbar' ] setup(name='tutorial', install_requires=requires, entry_points="""\ [paste.app_factory] main = tutorial:main [console_scripts] initialize_tutorial_db = tutorial.initialize_db:main """, )
Now we can initialize our project:
$ $VENV/bin/python setup.py develop
Our configuration file at
sqlroot/development.ini
wires together some new pieces:[app:main] use = egg:tutorial pyramid.reload_templates = true pyramid.includes = pyramid_debugtoolbar pyramid_tm sqlalchemy.url = sqlite:///%(here)s/sqltutorial.sqlite [server:main] use = egg:pyramid#wsgiref host = 0.0.0.0 port = 6543 # Begin logging configuration [loggers] keys = root, tutorial, sqlalchemy [logger_tutorial] level = DEBUG handlers = qualname = tutorial [logger_sqlalchemy] level = INFO handlers = qualname = sqlalchemy.engine [handlers] keys = console [formatters] keys = generic [logger_root] level = INFO handlers = console [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s # End logging configuration
The
setup.py
has an entry point for a console script atsqlroot/tutorial/initialize_db.py
, so let's add that script:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
import os import sys import transaction from sqlalchemy import engine_from_config from pyramid.paster import ( get_appsettings, setup_logging, ) from .models import ( DBSession, Root, Base, ) def usage(argv): cmd = os.path.basename(argv[0]) print('usage: %s <config_uri>\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): if len(argv) != 2: usage(argv) config_uri = argv[1] setup_logging(config_uri) settings = get_appsettings(config_uri) engine = engine_from_config(settings, 'sqlalchemy.') DBSession.configure(bind=engine) Base.metadata.create_all(engine) with transaction.manager: root = Root(title='My SQLTraversal Root') DBSession.add(root)
Our startup code in
sqlroot/tutorial/__init__.py
gets some bootstrapping changes:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
from pyramid.config import Configurator from sqlalchemy import engine_from_config from .models import ( DBSession, Base, root_factory ) def main(global_config, **settings): engine = engine_from_config(settings, 'sqlalchemy.') DBSession.configure(bind=engine) Base.metadata.bind = engine config = Configurator(settings=settings, root_factory=root_factory) config.include('pyramid_jinja2') config.scan('.views') return config.make_wsgi_app()
Create
sqlroot/tutorial/models.py
with our SQLAlchemy model for our persistent root:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
from sqlalchemy import ( Column, Integer, Text, ) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import ( scoped_session, sessionmaker, ) from zope.sqlalchemy import ZopeTransactionExtension DBSession = scoped_session( sessionmaker(extension=ZopeTransactionExtension())) Base = declarative_base() class Root(Base): __name__ = '' __parent__ = None __tablename__ = 'root' uid = Column(Integer, primary_key=True) title = Column(Text, unique=True) def root_factory(request): return DBSession.query(Root).one()
Let's run this console script, thus producing our database and table:
$ $VENV/bin/initialize_tutorial_db development.ini 2013-09-29 15:42:23,564 INFO [sqlalchemy.engine.base.Engine][MainThread] PRAGMA table_info("root") 2013-09-29 15:42:23,565 INFO [sqlalchemy.engine.base.Engine][MainThread] () 2013-09-29 15:42:23,566 INFO [sqlalchemy.engine.base.Engine][MainThread] CREATE TABLE root ( uid INTEGER NOT NULL, title TEXT, PRIMARY KEY (uid), UNIQUE (title) ) 2013-09-29 15:42:23,566 INFO [sqlalchemy.engine.base.Engine][MainThread] () 2013-09-29 15:42:23,569 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT 2013-09-29 15:42:23,572 INFO [sqlalchemy.engine.base.Engine][MainThread] BEGIN (implicit) 2013-09-29 15:42:23,573 INFO [sqlalchemy.engine.base.Engine][MainThread] INSERT INTO root (title) VALUES (?) 2013-09-29 15:42:23,573 INFO [sqlalchemy.engine.base.Engine][MainThread] ('My SQLAlchemy Root',) 2013-09-29 15:42:23,576 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
Nothing changes in our views or templates.
Run your Pyramid application with:
$ $VENV/bin/pserve development.ini --reload
Open http://localhost:6543/ in your browser.
Analysis¶
We perform the same kind of SQLAlchemy setup work that we saw in 19: Databases Using SQLAlchemy. In this case, our root factory returns an object from the database.
This models.Root
instance is the context
for our views and templates.
Rather than have our view and template code query the database, our root
factory gets the top and Pyramid does the rest by passing in a context
.
This point is illustrated by the fact that we didn't have to change our view logic or our templates. They depended on a context. Pyramid found the context and passed it into our views.
Extra Credit¶
- What will Pyramid do if the database doesn't have a
Root
that matches the SQLAlchemy query?