Flask-SQLAlchemy

Learning Goals

  • Build and run a Flask application on your computer.
  • Extend a Flask application to meet the unique requirements of different projects.

Key Vocab

  • Web Framework: software that is designed to support the development of web applications. Web frameworks provide built-in tools for generating web servers, turning Python objects into HTML, and more.
  • Extension: a package or module that adds functionality to a Flask application that it does not have by default.
  • Request: an attempt by one machine to contact another over the internet.
  • Client: an application or machine that accesses services being provided by a server through the internet.
  • Web Server: a combination of software and hardware that uses Hypertext Transfer Protocol (HTTP) and other protocols to respond to requests made over the internet.
  • Web Server Gateway Interface (WSGI): an interface between web servers and applications.
  • Template Engine: software that takes in strings with tokenized values, replacing the tokens with their values as output in a web browser.

Introduction

To get the most out of Flask, it is a good idea to connect it to a database for long-term storage of important data. Flask works well with SQLAlchemy and Alembic in their base forms as we saw in Phase 3, but we will be using libraries specially tailored to Flask: Flask-SQLAlchemy and Flask-Migrate. While these are mostly the same as SQLAlchemy and Alembic, there are a few important differences to note before you start developing your own applications.

We will explore the integration of these libraries with Flask applications as well as these differences in this lesson.


Flask Extensions

Flask is, on its own, a very minimal framework. It provides what we need to deliver Python code to the web, but that's about it. That being said, it is also designed to be extended. This allows you to choose the tools that you would like to use for database management, migrations, web forms, authentication, and much more.

Many Flask extensions, such as Flask-SQLAlchemy and Flask-Migrate, were created and are maintained by the Pallets Projects community. That being said, Flask is a Python web framework, and you can use any Python libraries that you think would benefit your project.


Managing Databases with Flask-SQLAlchemy

Run pipenv install; pipenv shell to install Flask, Flask-SQLAlchemy, and Flask-Migrate in your virtual environment. Enter the following in app.py:

# server/app.py

from flask import Flask

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

if __name__ == '__main__':
    app.run(port=5555, debug=True)

Just as with our other Flask applications, we start off with an application file that creates an instance of the Flask class with the module's name. We are also taking advantage of this opportunity to start configuring our database: in Phase 3, we would have defined this in alembic.ini. Since we're using Flask-Migrate instead of pure Alembic, we define the application configuration variables in the application itself.

Now that we have a basic application ready to run, let's configure some models.

Models with Flask-SQLAlchemy

The structure of models in Flask-SQLAlchemy is identical to that of SQLAlchemy models with one exception: rather than importing individual fields from the SQLAlchemy module, we import a SQLAlchemy class from Flask-SQLAlchemy that contains all of the same fields as attributes. This behaves similarly to declarative_base in SQLAlchemy.

Let's create some models!

# server/models.py

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Owner(db.Model):
    __tablename__ = 'owners'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, unique=True)

    pets = db.relationship('Pet', backref='owner')

    def __repr__(self):
        return f'<Pet Owner {self.name}>'

class Pet(db.Model):
    __tablename__ = 'pets'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    species = db.Column(db.String)

    owner_id = db.Column(db.Integer, db.ForeignKey('owners.id'))

    def __repr__(self):
        return f'<Pet {self.name}, {self.species}>'

As you can see, these are very similar to the models we set up in Phase 3. Rather than using a Base as a parent object for each of our models, we use the db object's Model class. Inside of the models, we retrieve important classes and methods through the db object. All classes that can be imported from vanilla SQLAlchemy can be accessed through the db object.

We can manually add these data models to the database, but there aren't many valid reasons to do that in your work. Instead, we will generate our database from our models using Flask-Migrate.

Flask-Migrate

Like Flask-SQLAlchemy with vanilla SQLAlchemy, Flask-Migrate is a wrapper for Alembic with minimal changes to allow it to integrate better with Flask applications. This can become a bit confusing, especially from the command line. Don't worry though- we'll discuss those nuances in detail here!

Modify app.py to mirror the following:

# server/app.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

from models import db

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

migrate = Migrate(app, db)

db.init_app(app)

if __name__ == '__main__':
    app.run(port=5555, debug=True)

We have imported the Migrate class here to set up our migrations using our Flask application instance and our SQLAlchemy instance. We also initialized our application for use within our database configuration with db.init_app(app).

NOTE: db.init_app(app) is an easy step to forget! (So don't!)

We're ready to get started with our migrations. The commands for Flask-Migrate are identical to those in Alembic, with the exception of using flask db in place of alembic in commands. Run the following from the server/ directory in the command line:

$ export FLASK_APP=app.py
$ export FLASK_RUN_PORT=5555
$ flask db init
# => /python-p4-flask-sqlalchemy/.venv/lib/python3.10/site-packages/flask_sqlalchemy/__init__.py:872: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
# =>   warnings.warn(FSADeprecationWarning(
# => /python-p4-flask-sqlalchemy/.venv/lib/python3.10/site-packages/flask_sqlalchemy/__init__.py:872: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
# =>   warnings.warn(FSADeprecationWarning(
# =>   Creating directory /python-p4-flask-sqlalchemy/server/migrations ...  done
# =>   Creating directory /python-p4-flask-sqlalchemy/server/migrations/versions ...  done
# =>   Generating /python-p4-flask-sqlalchemy/server/migrations/script.py.mako ...  done
# =>   Generating /python-p4-flask-sqlalchemy/server/migrations/env.py ...  done
# =>   Generating /python-p4-flask-sqlalchemy/server/migrations/README ...  done
# =>   Generating /python-p4-flask-sqlalchemy/server/migrations/alembic.ini ...  done
# =>   Please edit configuration/connection/logging settings in '/python-p4-flask-sqlalchemy/server/migrations/alembic.ini' before proceeding.

Let's heed that warning and set SQLALCHEMY_TRACK_MODIFICATIONS to False.

# server/app.py

...
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
...

We're getting a warning to change server/migrations/alembic.ini before we continue, but our server/app.py configuration already manages all of our unique application configuration variables. We can jump straight into migrating:

$ flask db revision --autogenerate -m'Create tables owners, pets'
# => INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
# => INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
# =>   Generating /python-p4-flask-sqlalchemy/server/migrations/versions/a48f1fc37e07_create_tables_owners_pets.py ...  done

...and pushing those migrations to our database:

$ flask db upgrade head
# => INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
# => INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
# => INFO  [alembic.runtime.migration] Running upgrade  -> 1c84830b3fc2, Create tables owners, pets

We've created a database with Flask-SQLAlchemy and Flask-Migrate! Open server/app.db (or server/instance/app.db, depending on your version of Flask-SQLAlchemy) and you should see the fruits of your labor:

Screenshot of SQLite database with three tables: alembic_version, owners, and pets


Conclusion

Flask extensions give us many opportunities to add functionality to our applications while writing minimal code for configurations. We saw how to create a database with Flask-SQLAlchemy and Flask-Migrate, and some among us may have even noticed that we wrote slightly less code than we did when using SQLAlchemy by itself!

In the next lesson, we will look into working with data in Flask applications using Flask-SQLAlchemy.


Solution Code

# server/app.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

from models import db

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

migrate = Migrate(app, db)

db.init_app(app)

if __name__ == '__main__':
    app.run(port=5555, debug=True)
# server/models.py

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Owner(db.Model):
    __tablename__ = 'owners'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, unique=True)

    pets = db.relationship('Pet', backref='owner')

    def __repr__(self):
        return f'<Pet Owner {self.name}>'

class Pet(db.Model):
    __tablename__ = 'pets'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    species = db.Column(db.String)

    owner_id = db.Column(db.Integer, db.ForeignKey('owners.id'))

    def __repr__(self):
        return f'<Pet {self.name}, {self.species}>'

Resources