Cookbook Documentation on working with databases and ORM's (namely, SQLAlchemy).
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user