Jonnie Grieve Digital Media: Blog

Home
by on 2nd September, 2021 - 1:04pm (0)

Blog: Integrating SQLAlchemy with Flask – #3 (More Posts)

In a new series of blogs related to SQLAlchemy, I’m moving away from the command line environment and moving on to exploring how Python, Flask, and SQLAlchemy can work together to make a website driven by external data.

Recap

When I left it in my last blog, there was an app that had a local development server with 4 interconnected routes that formed a small website.  This website contained a form, the idea of which would be to add the ability to add data to a database via a route with form elements.  By the end of this blog, I’ll have set up the ability to add that data. This has been a lot of fun to do and I hope you’ll learn a lot from it too. So let’s get started.

Setup

To get started we need to use the PIP package manager again in order to use Flask with SQLAlchemy.  So in the project root in the command line, type the following command.

pip install flask-sqlalchemy

And verify that it has in fact been installed with the following command.

pip freeze -> requirements.txt

I’m using Flask-SQLAlchemy==2.5.1 at the time of writing,  and in that file, if you see something like that listed in the file, you should be set to go.

It’s common practice in projects like this to use multiple files to run in component parts each with its own particular task, such as a file for the database model.  And that’s what we’re going to do here by moving some parts of the app.py file into a file called models.py.

# Declare imports for the app
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import datetime

# Connect to SQLAlchemy
app = Flask(__name__)

With SQLAlchemy-Flash installed, we can import it and write the code that connects the app to SQLAlchemy and assigns a new instance of it to a variable. So app.py no longer does this job on its own, but we’ll give it back that ability in a moment.

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///roster.db'
db = SQLAlchemy(app)

Next, we can store a reference to a database, (which in this case I’m calling “roster”) that will later be created with one of SQLAlchemey’s methods. I’ve already gone ahead and created this for the purposes of this blog, but if it doesn’t already exist it will be added to the project by the create_all() method on the reference to the database. In the second line above, I’m padding in a reference to that database using the variable name, “db”.

Here’s one issue that caught me out. In my experience, the file will only be created if you set up the string for the project root i.e. don’t try to put the file in its own directory such as “/db“. This can cause a 405 Method error and what is called an OperationalError exception. These issues go away if you define a file name in the root directory as specified above.

Below is the dunder main that contains the create_all() method.

if __name__ == '__main__':
    
    db.create_all()
    app.run(debug=True, port=8000, host='127.0.0.1')

Now it’s time for the part where we fashion the backend database by defining its schema, using a Python class.

# Create Database Model - 3 fields + ID as primary key
class Roster(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column("Name", db.String(80), unique=True, nullable=False)
    age = db.Column("Age", db.Integer, unique=True, nullable=False)
    joined = db.Column('Joined', db.DateTime, default=datetime.datetime.now)

    def __repr__(self):
        return f'''<Roster %r> ({self.name} 
        Age: {self.age}
        Joined: {self.joined }
       '''

Originally it was going to be a 2 column database plus the primary key field with 2 of the fields being explicitly entered by the user. But as you can see from above code, I’ve added a new field called “joined” which is a calculation using python’s datetime module as a way to provide the timestamp of the moment the data was added.  So this too will be used in the database despite not being entered by the user.  It is now a database of 4 columns.

So what’s next?

Let’s go back to app.py which is where our routes still live.

from flask import (render_template, url_for, request, redirect)

# import Database schema from models.py
from models import db, Roster, app

Earlier we moved the SQLAlchemy parts of the website out to another file. So when we did that we left the file out in the cold so it no longer knows anything about it or the model.

So above, what we’re doing is importing Flask and the modules from models.py back over to app.py.  The line, “from models” refers to models.py in the same directory as the file and all its modules so we can keep the routes and the model in separate locations but still working together as if they were one file.

Models.py contains the Roster() class as defined in the model.

app.route('/')
def index():
    roster = Roster.query.all()
    return render_template("index.html", roster=roster)

Now, in the index() method, we can run a query on the data we’ve just defined. This is just a simple one that returns all the data in the table.

But we also need a way to add data to the database. We need a method that sees when a form has been submitted and is ready to act when that happens.  And that’s what request.form does in your code. It’s the method used to capture form input.

Before, all the main_form() function did was to render the form template.  Now what’s happening is that a new variable (new_data) is being defined that stores an instance of the Roster class from models.py. It takes variable arguments that match those defined in the schema to which request.form methods are assigned. The string passed in should be the same as the name attribute of the form inputs. Those form inputs are located in the form route (‘/form’).

The next lines in the method are quite straightforward. We are adding and committing the data to SQLAlchemy and then redirecting the form to a new route. I’ve specified the home route. We really need a function that adds flash messages on a successful form entry, but you could also redirect to a route that contains such a message.

@app.route('/form', methods=["GET", "POST"])
def main_form():

    if request.form:
        # print(request.form)
        # print(request.form['name'])

        # add new record
        new_data = Roster(
            name=request.form['name'],
            age=request.form['age']
        )
            # joined=request.form['joined'])

        db.session.add(new_data)
        db.session.commit()
    return redirect(url_for('index'))

return render_template("form.html")

In the index route, I previously added an argument to the render template that assigns “roster” to a SQLAlchemy query of the same name. Confusingly it goes roster=roster. In this case, on the left hand side of the assignment, roster is a reference to the block that loops the data. And the right hand side of the assignment is a reference to the select all query.

@app.route('/')
def index():


roster = Roster.query.all()
return render_template("index.html", roster=roster)

This is how Flask knows to hook up the data to the browser and knows it needs to display the results of a query. Without it there’s no access to the data.

In the index.html template, Flask provides code blocks for looping through multiple records. Here’s an example of the syntax

{% for working_variablein iterable %}

{% endfor %}

Using the {% for %} code block we can iterate the results of an iterable such as a Class or a List. And what we have in our schema is a Class and a variable that we can use to iterate through called “roster”.  We’re going to use this to replace the multiple table row <tr> elements in the table with just one table row that is wrapped in a Flask looping content block.

e.g.

<table>
    <tr>
        <th>Name:</th>
        <th>Age:</th>
        <th>Date:</th>
    </tr>
    {% for people in roster %}
    <tr>
        <td>{{ people.name }}</td>
        <td>{{ people.age }}</td>
        <td>{{ people.joined }}</td>
    </tr>
    {% endfor %}
</table>

So instead we simply have one table row, that is looped for as many records as there are in the table.  people is a tracking variable that is linked to the roster query using the Roster model. So it’s not just any instance of Roster, it is the assigned variable that contains the simple “Select All” query.

Inside the table cells, we use the tracking variable to gain access again to the query and identify the columns we need. Again we use the variables as defined in the Roster model.

{{ people.name }}
{{ people.age }}
{{ people.joined }}

e.g.models.py

# Create Database Model - 3 fields + ID as primary key
class Roster(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column("Name", db.String(80), unique=True, nullable=False)
    age = db.Column("Age", db.Integer, unique=True, nullable=False)
    joined = db.Column('Joined', db.DateTime, default=datetime.datetime.now)

    def __repr__(self):
        return f'''<Roster %r> ({self.name} 
        Age: {self.age}
        Joined: {self.joined }
       '''

And if we’ve done this right, we end up with something like this.

Flask,: Integrating SQLAlchemy Data into Flask

Flask: Integrating SQLAlchemy Data into Flask

 

As you can see from the above screenshot we have 5 records of data (that’d I’d previously entered) via a simple HTML form.  We’ve taken advantage of the request.form method provided by Flask to get that data. And since we’re including it in a Python Class instance and committing the records with SQLAlchemy that data goes straight into a binary .db file. We then run a query in one of the Flask template files and a Flask content block to display the data in a useful way.

I’ll leave it there for now. This is the very basics of reading data with Flask and SQLAlchemy.  That’s fun and useful in itself but there’s still a lot more to do.  We want a way to update and/or delete records too via the front end. We’ll work on those next.

This post has been assigned to the following categories

    Leave a Reply

    Your email address will not be published. Required fields are marked *