Web Applications with SQLite
SQLite is a database engine that is distributed with Python and supports most of the SQL standard. It is ideal for developing database backed applications and because it uses the same SQL as larger systems, it is generally easy to port applications to a production database like MySQL or Oracle for deployment.
This chapter discusses the development of a simple application that stores data in an SQLite database and queries the database to generate content in pages.
Connecting to the Database
As described in the earlier chapter, SQLite stores databases as files and connecting to the database creates the file if it doesn't already exist. In a web application we need to maintain an open connection to the database so that the handler for any request that comes in can use the database to retrieve data to populate the page that is returned. One important thing to remember is that a web application is handling many requests for multiple users and each request handler might need to query and possibly update the database. This means that we need to manage connections to the database carefully.
A good solution to this problem is to use the bottle sqlite plugin which creates database connections for us and arranges to pass them to route handlers as required.
import bottle
app = bottle.Bottle()
plugin = bottle.ext.sqlite.Plugin(dbfile='test.db')
app.install(plugin)
@app.route('/show/:item')
def show(item, db):
cursor = db.cursor()
cursor.execute('SELECT id, description from items where name=?', item)
row = cursor.fetchone()
if row:
return template('showitem', page=row)
return HTTPError(404, "Page not found")
As this example adapted from the plugin web page
shows, we first configure the plugin with the name of the database file and
then install the plugin in our app. Then, we give our handler an extra
argument db
(it can go at the start or end of the argument list). The
plugin arranges for db
to be an open connection to the database.
The plugin is smart in that it ensures that any outstanding transactions are committed at the end of the request and rolls back changes if the request fails for any reason. The plugin makes managing database connections much easier when writing Bottle applications.
A Web Application
Building on our previous example in the cookies chapter, we'll build a sample application that presents the user with a form asking what they like, but this time store the submitted likes in a database. We'll display a list of the liked things below the form on the page.
We start with a basic application that accepts a form submission similar to that used in earlier chapters. We'll extend this so that the form data is stored in the database and displayed on the page.
The desired behaviour is that when we get a form submission, we store the value submitted in the database and when we generate a response page we include a list of database entries. To achieve this, we'll write two procedures to store and read data from the database. We'll also need to write a procedure to create the tables in the database. All of these procedures make a kind of abstract interface to the database - we put all of the SQL queries here and then call the procedures from within the main application. First, let's look at the procedure to create the database tables.
def create_table(db):
"""Create database table for the likes application
given a database connection 'db'.
Removes any existing data that might be in the
database."""
cursor = db.cursor()
cursor.execute("DROP TABLE IF EXISTS likes")
cursor.execute("""
CREATE TABLE likes (
thing text
)
""")
This procedure just runs the SQL code to first drop the table if it exits, and then create the table. This ensures that we start fresh with no old data, which is good for development since we want to be able to test with a consistent database state. Next, we'll look at the procedure to store a new entry in the database.
def store_like(db, like):
"""Store a new like in the database"""
cursor = db.cursor()
cursor.execute("INSERT INTO likes (thing) VALUES (?)", [like])
db.commit()
Again, the procedure is just running a bit of SQL code to insert the
data. Note the use of the ? template to insert the value into the SQL
string. In this case, there is only one value but the second argument to
cursor.execute
must be a list or tuple; in this case I've used a list.
Finally, the procedure to fetch data from the database returns all of the current entries as a list.
def get_likes(db):
"""Return a list of likes from the database"""
cursor = db.cursor()
cursor.execute("SELECT thing FROM likes")
result = []
for row in cursor:
result.append(row['thing'])
return result
The procedure can't simply return the result of cursor.fetchall
since
this will be a list of Row
objects, each containing a single value. The for
loop in my procedure iterates over the rows returned from the query and
pulls out the thing
field of each row, appending it to the result
list. If there are no rows returned from the query, the result list will
just be empty, which is appropriate behaviour.
Now that we have the building blocks for reading and writing the database, we can finish writing the application. To generate the main page, we just look in the database for the list of likes rather than the cookie:
@app.route('/')
def index(db):
"""Home page"""
info = {
'title': 'Welcome Home!',
'likes': get_likes(db)
}
return template('dblikes.tpl', info)
Note that since get_likes
returns a list of strings, I need to modify
the template we used in the cookie example where I could only like one
thing at a time. I can do this by inserting the following code into the
template:
<p>Things you like:</p>
% if likes:
<ul>
% for like in likes:
<li>{{like}}</li>
% end
</ul>
% end
The handler for the form submission also needs to change to store the like into the database rater than the cookie. Here's the code:
@app.post('/likes')
def like(db):
"""Process like form post request"""
# get the form field
likes = request.forms.get('likes')
if likes:
store_like(db, likes)
return redirect('/')
Note that since we've written a nice interface to the database, the application code itself is quite concise. It is much better practice to write code like this rather than including SQL code inside the application procedure. It also gives us more testable code since the database interface can be tested independently of the application itself.
We still need to make a call to create_table
at some point to make
sure the database is established. This should be before we try to do any
reading or writing of the database and we should only create the
database once, so the call should go in the 'main' part of the code.
We also need to initialise the bottle plugin. I've put both fragments of
code in the same place in this case to simplify things:
if __name__ == "__main__":
# code to connect to the database and create the tables
DATABASE_NAME = 'test.db'
db = sqlite3.connect(DATABASE_NAME)
create_tables(db)
# code to run our web application
plugin = bottle.ext.sqlite.Plugin(dbfile=DATABASE_NAME)
app.install(plugin)
# run the application
app.run()
(Note this might seem confusing. I'm connecting to the database to run the
create_tables
function but then connecting again in the plugin. This is
required because these are really two separate operations.)
This way, every time we run the application, a new, empty, database is
created. If you wanted to maintain the contents of the database between
runs, you would arrange to run create_table
only once or write it so
that it doesn't remove any existing data. One common solution is to
put the database creation code into a separate module that you can run
once to create or reset the database. You could then remove that code from
the main web application module.
That completes our little database backed application. It provides a model for any database backed application that you might write. The read and write procedures might get more complicated and there might be more of them if there is more than one database table, but the general idea should carry through to more complicated applications.