Basic Usage

This narrative documentation covers the use case of developing an application from scratch that uses mortar_rdb to interact with a relational database through development and testing.

Development

For this narrative, we’ll assume we’re developing our application in a python package called sample that uses the following model:

sample/model.py

from mortar_rdb import declarative_base
from mortar_rdb.controlled import Config, scan
from sqlalchemy import Table, Column, Integer, String

Base = declarative_base()

class User(Base):
  __tablename__ = 'user'
  id = Column(Integer, primary_key=True)
  name = Column(String(20))

source = scan('sample')
config = Config(source)

There’s nothing particularly special about this model other than that we’ve used mortar_rdb.declarative_base() to obtain a declarative base rather than calling sqlalchemy.ext.declarative.declarative_base(). This means that multiple python packages can all use the same declarative base, without having to worry about which package first defines the base.

This also means that all tables and models used in our application, regardless of the package they are defined in, can refer to each other.

To allow us to take advantage of the schema controls provided by mortar_rdb, we have also defined a Config with a Source returned from a scan(). The source is defined seperately to the configuration for two reasons:

  • it allows a configuration in another package to use the source defined here, which encapsulates all the tables managed by this package.
  • it makes it easier to write tests for migration scripts for the tables managed by this package.

To use the above model, we have the following view code:

sample/views.py

from mortar_rdb import get_session
from sample.model import User

def add_user(name):
    session = get_session()
    session.add(User(name=name))

def edit_user(id,name):
    session = get_session()
    user = session.query(User).filter(User.id == id).one()
    user.name = name

When using mortar_rdb, the session is obtained by calling mortar_rdb.get_session(). This allows the provision of the session to be independent of its use, which makes testing and deploying to different environments easier.

It is also advised that application code does not manage committing or rollback of database transactions via the session unless absolutely necessary. These actions should be the responsibility of the framework running the application.

For the purposes of this narrative, we will use the following micro framework:

sample/run.py

from mortar_rdb import register_session
from sample import views
from sample.config import db_url
from sample.model import config

import sys
import transaction

def main():
    register_session(db_url)
    name = sys.argv[1]
    args = sys.argv[2:]
    with transaction.manager:
        getattr(views, name)(*args)
    print("Ran %r with %r" % (name, args))

if __name__=='__main__':
    main()

Although there’s not much to it, the above framework shows the elements you will need to plug in to whatever framework you choose to use.

The main one of these is the call to register_session() which sets up the components necessary for get_session() to return a Session object.

The example framework is also shown to manage these sessions using the transaction package. Should your framework not use this package, you are strongly suggested to read the documentation for register_session() in detail to make sure you pass the correct parameters to get the behaviour required by your framework.

Testing

It’s alway a good idea to write automated tests, preferably before writing the code under test. mortar_rdb aids this by providing the mortar_rdb.testing module.

The following example shows how to provides minimal coverage using mortar_rdb.testing.register_session() and illustrates how the abstraction of configuring a session from obtaining a session in mortar_rdb makes testing easier:

sample/tests.py

from mortar_rdb import get_session
from mortar_rdb.testing import register_session
from sample.model import User, config
from sample.views import add_user, edit_user
from unittest import TestCase

class Tests(TestCase):

     def setUp(self):
         self.session = register_session(config=config)

     def tearDown(self):
         self.session.rollback()

     def test_add_user(self):
         # code under test
         add_user('Mr Test')
         # checks
         user = self.session.query(User).one()
         self.assertEqual('Mr Test', user.name)

     def test_edit_user(self):
         # setup
         self.session.add(User(id=1, name='Mr Chips'))
         # code under test
         edit_user('1', 'Mr Fish')
         # checks
         user = self.session.query(User).one()
         self.assertEqual('Mr Fish', user.name)

If you wish to run these tests against a particular database, rather than using the default in-memory SQLite database, then set the DB_URL enviroment variable to the SQLAlchemy url of the database you’d like to use. For example, if you run your tests with nose and are developing in a unix-like environment against a MySQL database, you could do:

$ DB_URL=mysql://scott:tiger@localhost/test nosetests

Release

With the application developed and tested, it is now time to release and deploy it. Users of mortar_rdb are encouraged to create a small database management script making use of mortar_rdb.controlled.Scripts.

Here’s is an example for the above model:

sample/db.py

from mortar_rdb.controlled import Scripts
from sample.config import db_url, is_production
from sample.model import config

scripts = Scripts(db_url, config, not is_production)

if __name__=='__main__':
    scripts()

This script can be used to create all tables required by the applications Config as follows:

$ bin/db create
For database at sqlite:////test.db:
Creating the following tables:
user

Other commands are are provided by Scripts and both the command line help, obtained with the --help option to either the script or any of its commands, and documentation are well worth a read.

So, the view code, database model, tests and framework are all now ready and the database has been created. The framework is now ready to use:

$ bin/run add_user test
Ran 'add_user' with ['test']