Tutorial

How To Use One-to-Many Database Relationships with Flask and SQLite

How To Use One-to-Many Database Relationships with Flask and SQLite

The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

Introduction

Flask is a framework for building web applications using the Python language, and SQLite is a database engine that can be used with Python to store application data. In this tutorial, you will use Flask with SQLite to create a to-do application where users can create lists of to-do items. You will learn how to use SQLite with Flask and how one-to-many database relationships work.

A one-to-many database relationship is a relationship between two database tables where a record in one table can reference several records in another table. For example, in a blogging application, a table for storing posts can have a one-to-many relationship with a table for storing comments. Each post can reference many comments, and each comment references a single post; therefore, one post has a relationship with many comments. The post table is a parent table, while the comments table is a child table—a record in the parent table can reference many records in the child table. This is important to be able to have access to related data in each table.

We’ll use SQLite because it is portable and does not need any additional set up to work with Python. It is also great for prototyping an application before moving to a larger database such as MySQL or Postgres. For more on how to choose the right database system read our SQLite vs MySQL vs PostgreSQL: A Comparison Of Relational Database Management Systems article.

Prerequisites

Before you start following this guide, you will need:

Step 1 — Creating the Database

In this step, you will activate your programming environment, install Flask, create the SQLite database, and populate it with sample data. You’ll learn how to use foreign keys to create a one-to-many relationship between lists and items. A foreign key is a key used to associate a database table with another table, it is the link between the child table and its parent table.

If you haven’t already activated your programming environment, make sure you’re in your project directory (flask_todo) and use this command to activate it:

  1. source env/bin/activate

Once your programming environment is activated, install Flask using the following command:

  1. pip install flask

Once the installation is complete, you can now create the database schema file that contains SQL commands to create the tables you need to store your to-do data. You will need two tables: a table called lists to store to-do lists, and an items table to store the items of each list.

Open a file called schema.sql inside your flask_todo directory:

  1. nano schema.sql

Type the following SQL commands inside this file:

flask_todo/schema.sql
DROP TABLE IF EXISTS lists;
DROP TABLE IF EXISTS items;

CREATE TABLE lists (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    title TEXT NOT NULL
);

CREATE TABLE items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    list_id INTEGER NOT NULL,
    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    content TEXT NOT NULL,
    FOREIGN KEY (list_id) REFERENCES lists (id)
);

Save and close the file.

The first two SQL command are DROP TABLE IF EXISTS lists; and DROP TABLE IF EXISTS items;, these delete any already existing tables named lists and items so you don’t see confusing behavior. Note that this will delete all of the content you have in the database whenever you use these SQL commands, so ensure you don’t write any important content in the web application until you finish this tutorial and experiment with the final result.

Next, you use CREATE TABLE lists to create the lists table that will store the to-do lists (such as a study list, work list, home list, and so on) with the following columns:

  • id: An integer that represents a primary key, this will get assigned a unique value by the database for each entry (i.e. to-do list).
  • created: The time the to-do list was created at. NOT NULL signifies that this column should not be empty and the DEFAULT value is the CURRENT_TIMESTAMP value, which is the time at which the list was added to the database. Just like id, you don’t need to specify a value for this column, as it will be automatically filled in.
  • title: The list title.

Then, you create a table called items to store to-do items. This table has an ID, a list_id integer column to identify which list an item belongs to, a creation date, and the item’s content. To link an item to a list in the database you use a foreign key constraint with the line FOREIGN KEY (list_id) REFERENCES lists (id). Here the lists table is a parent table, which is the table that is being referenced by the foreign key constraint, this indicates a list can have multiple items. The items table is a child table, which is the table the constraint applies to. This means items belong to a single list. The list_id column references the id column of the lists parent table.

Since a list can have many items, and an item belongs to only one list, the relationship between the lists and items tables is a one-to-many relationship.

Next, you will use the schema.sql file to create the database. Open a file named init_db.py inside the flask_todo directory:

  1. nano init_db.py

Then add the following code:

flask_todo/init_db.py
import sqlite3

connection = sqlite3.connect('database.db')


with open('schema.sql') as f:
    connection.executescript(f.read())

cur = connection.cursor()

cur.execute("INSERT INTO lists (title) VALUES (?)", ('Work',))
cur.execute("INSERT INTO lists (title) VALUES (?)", ('Home',))
cur.execute("INSERT INTO lists (title) VALUES (?)", ('Study',))

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (1, 'Morning meeting')
            )

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (2, 'Buy fruit')
            )

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (2, 'Cook dinner')
            )

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (3, 'Learn Flask')
            )

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (3, 'Learn SQLite')
            )

connection.commit()
connection.close()

Save and close the file.

Here you connect to a file called database.db that will be created once you execute this program. You then open the schema.sql file and run it using the executescript() method that executes multiple SQL statements at once.

Running schema.sql will create the lists and items tables. Next, using a Cursor object, you execute a few INSERT SQL statements to create three lists and five to-do items.

You use the list_id column to link each item to a list via the list’s id value. For example, the Work list was the first insertion into the database, so it will have the ID 1. This is how you can link the Morning meeting to-do item to Work—the same rule applies to the other lists and items.

Finally, you commit the changes and close the connection.

Run the program:

  1. python init_db.py

After execution, a new file called database.db will appear in your flask_todo directory.

You’ve activated your environment, installed Flask, and created the SQLite database. Next, you’ll retrieve the lists and items from the database and display them in the application’s homepage.

Step 2 — Displaying To-do Items

In this step, you will connect the database you created in the previous step to a Flask application that displays the to-do lists and the items of each list. You will learn how to use SQLite joins to query data from two tables and how to group to-do items by their lists.

First, you will create the application file. Open a file named app.py inside the flask_todo directory:

  1. nano app.py

And then add the following code to the file:

flask_todo/app.py
from itertools import groupby
import sqlite3
from flask import Flask, render_template, request, flash, redirect, url_for


def get_db_connection():
    conn = sqlite3.connect('database.db')
    conn.row_factory = sqlite3.Row
    return conn


app = Flask(__name__)
app.config['SECRET_KEY'] = 'this should be a secret random string'


@app.route('/')
def index():
    conn = get_db_connection()
    todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
                          ON i.list_id = l.id ORDER BY l.title;').fetchall()

    lists = {}

    for k, g in groupby(todos, key=lambda t: t['title']):
        lists[k] = list(g)

    conn.close()
    return render_template('index.html', lists=lists)

Save and close the file.

The get_db_connection() function opens a connection to the database.db database file and then sets the row_factory attribute to sqlite3.Row. In this way you can have name-based access to columns; this means that the database connection will return rows that behave like regular Python dictionaries. Lastly, the function returns the conn connection object you’ll be using to access the database.

In the index() view function, you open a database connection and execute the following SQL query:

SELECT i.content, l.title FROM items i JOIN lists l ON i.list_id = l.id ORDER BY l.title;

You then retrieve its results by using the fetchall() method and save the data in a variable called todos.

In this query, you use SELECT to get the content of the item and the title of the list it belongs to by joining both the items and lists tables (with the table aliases i for items and l for lists). With the join condition i.list_id = l.id after the ON keyword, you will get each row from the items table with every row from the lists table where the list_id column of the items table matches the id of the lists table. You then use ORDER BY to order the results by list titles.

To understand this query better, open the Python REPL in your flask_todo directory:

  1. python

To understand the SQL query, examine the contents of the todos variable by running this small program:

  1. from app import get_db_connection
  2. conn = get_db_connection()
  3. todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
  4. ON i.list_id = l.id ORDER BY l.title;').fetchall()
  5. for todo in todos:
  6. print(todo['title'], ':', todo['content'])

You first import the get_db_connection from the app.py file then open a connection and execute the query (note that this is the same SQL query you have in your app.py file). In the for loop you print the title of the list and the content of each to-do item.

The output will be as follows:

Output
Home : Buy fruit Home : Cook dinner Study : Learn Flask Study : Learn SQLite Work : Morning meeting

Close the REPL using CTRL + D.

Now that you understand how SQL joins work and what the query achieves, let’s return back to the index() view function in your app.py file. After declaring the todos variable, you group the results using the following code:

lists = {}

for k, g in groupby(todos, key=lambda t: t['title']):
    lists[k] = list(g)

You first declare an empty dictionary called lists, then use a for loop to go through a grouping of the results in the todos variable by the list’s title. You use the groupby() function you imported from the itertools standard library. This function will go through each item in the todos variable and generate a group of results for each key in the for loop.

k represents list titles (that is, Home, Study, Work), which are extracted using the function you pass to the key parameter of the groupby() function. In this case the function is lambda t: t['title'] that takes a to-do item and returns the title of the list (as you have done before with todo['title'] in the previous for loop). g represents the group that contains the to-do items of each list title. For example, in the first iteration, k will be 'Home', while g is an iterable that will contain the items 'Buy fruit' and 'Cook dinner'.

This gives us a representation of the one-to-many relationship between lists and items, where each list title has several to-do items.

When running the app.py file, and after the for loop finishes execution, lists will be as follows:

Output
{'Home': [<sqlite3.Row object at 0x7f9f58460950>, <sqlite3.Row object at 0x7f9f58460c30>], 'Study': [<sqlite3.Row object at 0x7f9f58460b70>, <sqlite3.Row object at 0x7f9f58460b50>], 'Work': [<sqlite3.Row object at 0x7f9f58460890>]}

Each sqlite3.Row object will contain the data you retrieved from the items table using the SQL query in the index() function. To represent this data better, let’s make a program that goes through the lists dictionary and displays each list and its items.

Open a file called list_example.py in your flask_todo directory:

  1. nano list_example.py

Then add the following code:

flask_todo/list_example.py

from itertools import groupby
from app import get_db_connection

conn = get_db_connection()
todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
                        ON i.list_id = l.id ORDER BY l.title;').fetchall()

lists = {}

for k, g in groupby(todos, key=lambda t: t['title']):
    lists[k] = list(g)

for list_, items in lists.items():
    print(list_)
    for item in items:
        print('    ', item['content'])

Save and close the file.

This is very similar to the content in your index() view function. The last for loop here illustrates how the lists dictionary is structured. You first go through the dictionary’s items, print the list title (which is in the list_ variable), then go through each group of to-do items that belong to the list and print the content value of the item.

Run the list_example.py program:

  1. python list_example.py

Here is the output of list_example.py:

Output
Home Buy fruit Cook dinner Study Learn Flask Learn SQLite Work Morning meeting

Now that you understand each part of the index() function, let’s create a base template and create the index.html file you rendered using the line return render_template('index.html', lists=lists).

In your flask_todo directory, create a templates directory and open a file called base.html inside it:

  1. mkdir templates
  2. nano templates/base.html

Add the following code inside base.html, note that you’re using Bootstrap here. If you are not familiar with HTML templates in Flask, see Step 3 of How To Make a Web Application Using Flask in Python 3:

flask_todo/templates/base.html
<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>{% block title %} {% endblock %}</title>
  </head>
  <body>
    <nav class="navbar navbar-expand-md navbar-light bg-light">
        <a class="navbar-brand" href="{{ url_for('index')}}">FlaskTodo</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
            <li class="nav-item active">
                <a class="nav-link" href="#">About</a>
            </li>
            </ul>
        </div>
    </nav>
    <div class="container">
        {% block content %} {% endblock %}
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>

Save and close the file.

Most of the code in the preceding block is standard HTML and code required for Bootstrap. The <meta> tags provide information for the web browser, the <link> tag links the Bootstrap CSS files, and the <script> tags are links to JavaScript code that allows some additional Bootstrap features. Check out the Bootstrap documentation for more information.

Next, create the index.html file that will extend this base.html file:

  1. nano templates/index.html

Add the following code to index.html:

flask_todo/templates/index.html
{% extends 'base.html' %}

{% block content %}
    <h1>{% block title %} Welcome to FlaskTodo {% endblock %}</h1>
    {% for list, items in lists.items() %}
        <div class="card" style="width: 18rem; margin-bottom: 50px;">
            <div class="card-header">
                <h3>{{ list }}</h3>
            </div>
            <ul class="list-group list-group-flush">
                {% for item in items %}
                    <li class="list-group-item">{{ item['content'] }}</li>
                {% endfor %}
            </ul>
        </div>
    {% endfor %}
{% endblock %}

Here you use a for loop to go through each item of the lists dictionary, you display the list title as a card header inside an <h3> tag, and then use a list group to display each to-do item that belongs to the list in an <li> tag. This follows the same rules explained in the list_example.py program.

You will now set the environment variables Flask needs and run the application using the following commands:

  1. export FLASK_APP=app
  2. export FLASK_ENV=development
  3. flask run

Once the development server is running, you can visit the URL http://127.0.0.1:5000/ in your browser. You will see a web page with the “Welcome to FlaskTodo” and your list items.

Home Page

You can now type CTRL + C to stop your development server.

You’ve created a Flask application that displays the to-do lists and the items of each list. In the next step, you will add a new page for creating new to-do items.

Step 3 — Adding New To-do Items

In this step, you will make a new route for creating to-do items, you will insert data into database tables, and associate items with the lists they belong to.

First, open the app.py file:

  1. nano app.py

Then, add a new /create route with a view function called create() at the end of the file:

flask_todo/app.py
...
@app.route('/create/', methods=('GET', 'POST'))
def create():
    conn = get_db_connection()
    lists = conn.execute('SELECT title FROM lists;').fetchall()

    conn.close()
    return render_template('create.html', lists=lists)

Save and close the file.

Because you will use this route to insert new data to the database via a web form, you allow both GET and POST requests using methods=('GET', 'POST') in the app.route() decorator. In the create() view function, you open a database connection then get all the list titles available in the database, close the connection, and render a create.html template passing it the list titles.

Next, open a new template file called create.html:

  1. nano templates/create.html

Add the following HTML code to create.html:

flask_todo/templates/create.html
{% extends 'base.html' %}

{% block content %}
<h1>{% block title %} Create a New Item {% endblock %}</h1>

<form method="post">
    <div class="form-group">
        <label for="content">Content</label>
        <input type="text" name="content"
               placeholder="Todo content" class="form-control"
               value="{{ request.form['content'] }}"></input>
    </div>

    <div class="form-group">
        <label for="list">List</label>
        <select class="form-control" name="list">
            {% for list in lists %}
                {% if list['title'] == request.form['list'] %}
                    <option value="{{ request.form['list'] }}" selected>
                        {{ request.form['list'] }}
                    </option>
                {% else %}
                    <option value="{{ list['title'] }}">
                        {{ list['title'] }}
                    </option>
                {% endif %}
            {% endfor %}
        </select>
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>
{% endblock %}

Save and close the file.

You use request.form to access the form data that is stored in case something goes wrong with your form submission (for example, if no to-do content was provided). In the <select> element, you loop through the lists you retrieved from the database in the create() function. If the list title is equal to what is stored in request.form then the selected option is that list title, otherwise, you display the list title in a normal non-selected <option> tag.

Now, in the terminal, run your Flask application:

  1. flask run

Then visit http://127.0.0.1:5000/create in your browser, you will see a form for creating a new to-do item, note that the form doesn’t work yet because you have no code to handle POST requests that get sent by the browser when submitting the form.

Type CTRL + C to stop your development server.

Next, let’s add the code for handling POST requests to the create() function and make the form function properly, open app.py:

  1. nano app.py

Then edit the create() function to look like so:

flask_todo/app.py
...
@app.route('/create/', methods=('GET', 'POST'))
def create():
    conn = get_db_connection()

    if request.method == 'POST':
        content = request.form['content']
        list_title = request.form['list']

        if not content:
            flash('Content is required!')
            return redirect(url_for('index'))

        list_id = conn.execute('SELECT id FROM lists WHERE title = (?);',
                                 (list_title,)).fetchone()['id']
        conn.execute('INSERT INTO items (content, list_id) VALUES (?, ?)',
                     (content, list_id))
        conn.commit()
        conn.close()
        return redirect(url_for('index'))

    lists = conn.execute('SELECT title FROM lists;').fetchall()

    conn.close()
    return render_template('create.html', lists=lists)

Save and close the file.

Inside the request.method == 'POST' condition you get the to-do item’s content and the list’s title from the form data. If no content was submitted, you send the user a message using the flash() function and redirect to the index page. If this condition was not triggered, then you execute a SELECT statement to get the list ID from the provided list title and save it in a variable called list_id. You then execute an INSERT INTO statement to insert the new to-do item into the items table. You use the list_id variable to link the item to the list it belongs to. Finally, you commit the transaction, close the connection, and redirect to the index page.

As a last step, you will add a link to /create in the navigation bar and display flashed messages below it, to do this, open base.html:

  1. nano templates/base.html

Edit the file by adding a new <li> navigation item that links to the create() view function. Then display the flashed messages using a for loop above the content block. These are available in the get_flashed_messages() Flask function:

flask_todo/templates/base.html
<nav class="navbar navbar-expand-md navbar-light bg-light">
    <a class="navbar-brand" href="{{ url_for('index')}}">FlaskTodo</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNav">
        <ul class="navbar-nav">
        <li class="nav-item active">
            <a class="nav-link" href="{{ url_for('create') }}">New</a>
        </li>

        <li class="nav-item active">
            <a class="nav-link" href="#">About</a>
        </li>
        </ul>
    </div>
</nav>
<div class="container">
    {% for message in get_flashed_messages() %}
        <div class="alert alert-danger">{{ message }}</div>
    {% endfor %}
    {% block content %} {% endblock %}
</div>

Save and close the file.

Now, in the terminal, run your Flask application:

  1. flask run

A new link to /create will appear in the navigation bar. If you navigate to this page and try to add a new to-do item with no content, you’ll receive a flashed message saying Content is required!. If you fill in the content form, a new to-do item will appear on the index page.

In this step, you have added the ability to create new to-do items and save them to the database.

You can find the source code for this project in this repository.

Conclusion

You now have an application to manage to-do lists and items. Each list has several to-do items and each to-do item belongs to a single list in a one-to-many relationship. You learned how to use Flask and SQLite to manage multiple related database tables, how to use foreign keys and how to retrieve and display related data from two tables in a web application using SQLite joins.

Furthermore, you grouped results using the groupby() function, inserted new data to the database, and associated database table rows with the tables they are related to. You can learn more about foreign keys and database relationships from the SQLite documentation.

You can also read more of our Python Framework content. If you want to check out the sqlite3 Python module, read our tutorial on How To Use the sqlite3 Module in Python 3.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors


Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.