summaryrefslogtreecommitdiff
path: root/docs/source/databases.rst
blob: 90f2522821c6debf0a58bdee4dc84935a707d612 (plain)
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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.