.. _qtut_databases:
|
|
==============================
|
19: Databases Using SQLAlchemy
|
==============================
|
|
Store and retrieve data using the SQLAlchemy ORM atop the SQLite database.
|
|
|
Background
|
==========
|
|
Our Pyramid-based wiki application now needs database-backed storage of pages.
|
This frequently means an SQL database. The Pyramid community strongly supports
|
the :ref:`SQLAlchemy <sqla:index_toplevel>` project and its
|
:ref:`object-relational mapper (ORM) <sqla:ormtutorial_toplevel>` as a
|
convenient, Pythonic way to interface to databases.
|
|
In this step we hook up SQLAlchemy to a SQLite database table, providing
|
storage and retrieval for the wiki pages in the previous step.
|
|
.. note::
|
|
The Pyramid cookiecutter ``pyramid-cookiecutter-starter`` is really
|
helpful for getting a SQLAlchemy project going, including generation of
|
the console script. Since we want to see all the decisions, we will forgo
|
convenience in this tutorial, and wire it up ourselves.
|
|
|
Objectives
|
==========
|
|
- Store pages in SQLite by using SQLAlchemy models.
|
|
- Use SQLAlchemy queries to list/add/view/edit pages.
|
|
- Provide a database-initialize command by writing a Pyramid *console script*
|
which can be run from the command line.
|
|
|
Steps
|
=====
|
|
#. We are going to use the forms step as our starting point:
|
|
.. code-block:: bash
|
|
cd ..; cp -r forms databases; cd databases
|
|
#. We need to add some dependencies in ``databases/setup.py`` as well as an :term:`entry point` for the command-line script:
|
|
.. literalinclude:: databases/setup.py
|
:linenos:
|
:emphasize-lines: 9-10, 12, 34-36
|
|
.. note:: We aren't yet doing ``$VENV/bin/pip install -e .`` because we need to write a script and update configuration first.
|
|
#. Our configuration file at ``databases/development.ini`` wires together some new pieces:
|
|
.. literalinclude:: databases/development.ini
|
:language: ini
|
|
#. This engine configuration now needs to be read into the application through changes in ``databases/tutorial/__init__.py``:
|
|
.. literalinclude:: databases/tutorial/__init__.py
|
:linenos:
|
|
#. Make a command-line script at ``databases/tutorial/initialize_db.py`` to initialize the database:
|
|
.. literalinclude:: databases/tutorial/initialize_db.py
|
:linenos:
|
|
#. Now that we've got all the pieces ready, and because we changed ``setup.py``, we now install all the goodies:
|
|
.. code-block:: bash
|
|
$VENV/bin/pip install -e .
|
|
#. The script references some models in ``databases/tutorial/models.py``:
|
|
.. literalinclude:: databases/tutorial/models.py
|
:linenos:
|
|
#. Let's run this console script, thus producing our database and table:
|
|
.. code-block:: bash
|
|
$VENV/bin/initialize_tutorial_db development.ini
|
|
2016-04-16 13:01:33,055 INFO [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
|
2016-04-16 13:01:33,055 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
|
2016-04-16 13:01:33,056 INFO [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
|
2016-04-16 13:01:33,056 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
|
2016-04-16 13:01:33,057 INFO [sqlalchemy.engine.base.Engine][MainThread] PRAGMA table_info("wikipages")
|
2016-04-16 13:01:33,057 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
|
2016-04-16 13:01:33,058 INFO [sqlalchemy.engine.base.Engine][MainThread]
|
CREATE TABLE wikipages (
|
uid INTEGER NOT NULL,
|
title TEXT,
|
body TEXT,
|
PRIMARY KEY (uid),
|
UNIQUE (title)
|
)
|
|
|
2016-04-16 13:01:33,058 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
|
2016-04-16 13:01:33,059 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
|
2016-04-16 13:01:33,062 INFO [sqlalchemy.engine.base.Engine][MainThread] BEGIN (implicit)
|
2016-04-16 13:01:33,062 INFO [sqlalchemy.engine.base.Engine][MainThread] INSERT INTO wikipages (title, body) VALUES (?, ?)
|
2016-04-16 13:01:33,063 INFO [sqlalchemy.engine.base.Engine][MainThread] ('Root', '<p>Root</p>')
|
2016-04-16 13:01:33,063 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
|
|
#. With our data now driven by SQLAlchemy queries, we need to update our ``databases/tutorial/views.py``:
|
|
.. literalinclude:: databases/tutorial/views.py
|
:linenos:
|
|
#. Our tests in ``databases/tutorial/tests.py`` changed to include SQLAlchemy bootstrapping:
|
|
.. literalinclude:: databases/tutorial/tests.py
|
:linenos:
|
|
#. Run the tests in your package using ``pytest``:
|
|
.. code-block:: bash
|
|
$VENV/bin/pytest tutorial/tests.py -q
|
..
|
2 passed in 1.41 seconds
|
|
#. Run your Pyramid application with:
|
|
.. code-block:: bash
|
|
$VENV/bin/pserve development.ini --reload
|
|
#. Open http://localhost:6543/ in a browser.
|
|
|
Analysis
|
========
|
|
Let's start with the dependencies. We made the decision to use ``SQLAlchemy``
|
to talk to our database. We also, though, installed ``pyramid_tm`` and
|
``zope.sqlalchemy``. Why?
|
|
Pyramid has a strong orientation towards support for ``transactions``.
|
Specifically, you can install a transaction manager into your application
|
either as middleware or a Pyramid "tween". Then, just before you return the
|
response, all transaction-aware parts of your application are executed.
|
|
This means Pyramid view code usually doesn't manage transactions. If your view
|
code or a template generates an error, the transaction manager aborts the
|
transaction. This is a very liberating way to write code.
|
|
The ``pyramid_tm`` package provides a "tween" that is configured in the
|
``development.ini`` configuration file. That installs it. We then need a
|
package that makes SQLAlchemy, and thus the RDBMS transaction manager,
|
integrate with the Pyramid transaction manager. That's what ``zope.sqlalchemy``
|
does.
|
|
Where do we point at the location on disk for the SQLite file? In the
|
configuration file. This lets consumers of our package change the location in a
|
safe (non-code) way. That is, in configuration. This configuration-oriented
|
approach isn't required in Pyramid; you can still make such statements in your
|
``__init__.py`` or some companion module.
|
|
The ``initialize_tutorial_db`` is a nice example of framework support. You
|
point your setup at the location of some ``[console_scripts]``, and these get
|
generated into your virtual environment's ``bin`` directory. Our console script
|
follows the pattern of being fed a configuration file with all the
|
bootstrapping. It then opens SQLAlchemy and creates the root of the wiki, which
|
also makes the SQLite file. Note the ``with transaction.manager`` part that
|
puts the work in the scope of a transaction, as we aren't inside a web request
|
where this is done automatically.
|
|
The ``models.py`` does a little bit of extra work to hook up SQLAlchemy into
|
the Pyramid transaction manager. It then declares the model for a ``Page``.
|
|
Our views have changes primarily around replacing our dummy
|
dictionary-of-dictionaries data with proper database support: list the rows,
|
add a row, edit a row, and delete a row.
|
|
|
Extra credit
|
============
|
|
#. Why all this code? Why can't I just type two lines and have magic ensue?
|
|
#. Give a try at a button that deletes a wiki page.
|