From cfc202a97605b92baf2f549e4d98bce13d2ae2eb Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Sun, 6 Mar 2011 13:58:04 -0500 Subject: [PATCH] Cookbook Documentation on working with databases and ORM's (namely, SQLAlchemy). --- docs/source/databases.rst | 138 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/docs/source/databases.rst b/docs/source/databases.rst index e69de29..90f2522 100644 --- a/docs/source/databases.rst +++ b/docs/source/databases.rst @@ -0,0 +1,138 @@ +.. _databases: + +Working with Databases and ORM's +============= +Out of the box, Pecan provides no opinionated support for working with databases, +but it's easy to hook into your ORM of choice with minimal effort. This article +details best practices for integration the popular Python ORM, SQLAlchemy, into +your Pecan project. + +init_model and Preparing Your Model +---------------- +Pecan's default quickstart project includes an empty stub directory for implementing +your model as you see fit:: + +. +└── test_project + ├── app.py + ├── __init__.py + ├── controllers + ├── model + │   ├── __init__.py + └── templates + +By default, this module contains a special method, ``init_model``:: + + from pecan import conf + + def init_model(): + # Read and parse database bindings from pecan.conf + pass + +The purpose of this method is to determine bindings from your configuration file and create +necessary engines, pools, etc... according to your ORM or database toolkit of choice. + +Additionally, your project's ``model`` module can be used to define functions for common binding +operations, such as starting transactions, committing or rolling back work, and clearing a Session. +This is also the location in your project where object and relation definitions should be defined. +Here's what a sample Pecan configuration file with database bindings might look like:: + + # Server Specific Configurations + server = { + ... + } + + # Pecan Application Configurations + app = { + ... + } + + # Bindings and options to pass to SQLAlchemy's ``create_engine`` + sqlalchemy = { + 'url' : 'mysql://root:@localhost/atrium?charset=utf8&use_unicode=0', + 'echo' : False, + 'echo_pool' : False, + 'pool_recycle' : 3600, + 'encoding' : 'utf-8' + } + +...and a basic model implementation that can be used to configure and bind using SQLAlchemy:: + + from pecan import conf + from sqlalchemy import create_engine, MetaData + from sqlalchemy.orm import scoped_session, sessionmaker + + Session = scoped_session(sessionmaker()) + metadata = MetaData() + + def _engine_from_config(configuration): + configuration = dict(configuration) + url = configuration.pop('url') + return create_engine(url, **configuration) + + def init_model(): + conf.sqlalchemy.engine = _engine_from_config(conf.sqlalchemy) + + def start(): + Session.bind = conf.sqlalchemy.engine + metadata.bind = Session.bind + + def commit(): + Session.commit() + + def rollback(): + Session.rollback() + + def clear(): + Session.remove() + +Binding Within the Application +---------------- +There are several approaches that can be taken to wrap your application's requests with calls +to appropriate model function calls. One approach is WSGI middleware. We also recommend +Pecan hooks (see ref:`hooks`). Pecan comes with ``TransactionHook``, a hook which can +be used to wrap requests in transactions for you. To use it, you simply include it in your +project's ``app.py`` file and pass it a set of functions related to database binding:: + + app = make_app( + conf.app.root, + static_root = conf.app.static_root, + template_path = conf.app.template_path, + debug = conf.app.debug, + hooks = [ + TransactionHook( + model.start, + model.start_read_only, + model.commit, + model.rollback, + model.clear + ) + ] + ) + +For the above example, on HTTP POST, PUT, and DELETE requests, ``TransactionHook`` behaves in the +following manner:: + +#. Before controller routing has been determined, ``model.start()`` is called. This function +should bind to the appropriate SQLAlchemy engine and start a transaction. +#. Controller code is run and returns. +#. If your controller or template rendering fails and raises an exception, ``model.rollback()`` +is called and the original exception is re-raised. This allows you to rollback your database +transaction to avoid committing work when exceptions occur in your application code. +#. If the controller returns successfully, ``model.commit()`` and ``model.clear()`` are called. + +On idempotent operations (like HTTP GET and HEAD requests), TransactionHook behaves in the following +manner:: + +#. ``model.start_read_only()`` is called. This function should bind to your SQLAlchemy engine. +#. Controller code is run and returns. +#. If the controller returns successfully, ``model.clear()`` is called. + +Splitting Reads and Writes +---------------- +Employing the above strategy with ``TransactionHook`` makes it very simple to split database +reads and writes based upon HTTP methods (i.e., GET/HEAD requests are read and would potentially +be routed to a read-only database slave, while POST/PUT/DELETE requests require writing, and +would bind to a master database with read/write priveleges) It's also very easy extend +``TransactionHook`` or write your own hook implementation for more refined control over where and +when database bindings are called. \ No newline at end of file