As we begin to explore databases, we also have to talk about data structures. When you build an application, one of the first things to think about is the kind of data your application needs and works with.
For example, most applications will need a collection of users where each user has at least the properties email
and password
. Maybe they also have properties like name
or profile_picture_url
. It may be tempting to try and set up more properties because you may need them in the future. But in fact, itâs considered best practice to work with as little data as possible for as long as you can. And later in this exercise, youâll learn a method to add properties later on easily.
Some other examples for data collections your application may need are articles, to-do list items, shop items, expenses.
Models (MVC)
One of the most popular ways to structure the code of your web application is called MVC (Model-View-Controller). It describes the way you organize the logic of your code. Models are in charge or representing a data collection. You can, for example, have a User
model or an Article
model. They are usually directly connected to a database table and represent the data in that table.
Views refer to the part of your application thatâs visual to users. In our Flask app, that would be the HTML templates.
Lastly, the controllers connect everything. We already added them to our application in the routes.py file. They are in charge of bringing everything together, models, views, and any business logic from other files. Controllers arenât supposed to have business logic in them. They are only there to connect the different parts of your application.
For this section, we will focus on models as models represent the data layer of your application. If we think of our cookie shop one obvious data set would be for the cookies. So in my application, Iâll need a Cookie
model. This model will replace the cookies_data
collection we used in the previous exercises.
Models are usually represented as Python classes, and their name is defined in singular. Thatâs just a common pattern, and weâll stick to it, too.
To kick things off, create a new file: /app/cookies/models.py
You should also add it to the /app/cookies/__init__.py file:
from . import routes, models
You can remove the cookies_data
variable in /app/cookies/routes.py now or comment it out and delete it at the end of the exercise. Keep in mind that deleting the cookie_data
will break the routes defined in that file. So you can also keep it in there and delete it in a later exercise when we fix the logic in the views.
In our new models.py file, create a new class called Cookie
. Give it a few properties like id
, slug
, name
, and price
but assign them to empty strings for now. They are just placeholders that will be filled in later on.
class Cookie():
id = ''
slug = ''
name = ''
price = ''
Those are the same values we had before. But this time, I specifically defined a slug
field. Additionally, I added an id
field. The ID is important for later. Every data item should have a unique ID. This way, youâll have an easier time finding a specific item.
Creating placeholder models like this can be a helpful way for you to think about the different data collections and their properties as you plan your application.
When or why use a database?
Now that we deleted the cookies_data
set, we need an alternative way to store the cookie data. We already mentioned above that we want to use a database for that. A database is usually a separate piece of software installed on your machine and on the server. You may have heard of MySQL, Postgres, or MongoDB. These are not just file formats. They are actual pieces of software that need to be installed and executed. And in order for an application to interact with a MySQL database, the MySQL software has to be running in parallel to your application at all times.
Databases are best suited for dynamic data and content. Those can be blog posts, users, bank accounts, just to name a few examples. However, it may make sense to use static data in some cases. If you have a set of data that most likely will never (or very rarely) change, itâs also totally fine not to use a database. Some examples for static data that doesnât need a database could be a list of countries you offer your products in, a list of services you offer that are never changing, the type of bank accounts a user could open, etc.
We may want to add or remove types of cookies as we go. So weâd like to store them dynamically in a database.
Choosing a database
Once we understand why to use a database, we must choose which one to use. Weâre not going to go into the different kinds of databases here. But just know that with Flask, youâre free to use any type of database you like.
Weâll go with a relational database because itâs a very common choice in the Flask community. If youâre new to the field, itâs always good to choose a technology stack thatâs widely used. This way, itâll be easier for you to get help whenever you get stuck. Each tech community has its preferred stack. PHP developers like MySQL, Node.JS developers like MongoDB, and Flask developers often use Postgres. You can use any kind of database in all these cases. But Postgres is just a common choice among Flask developers.
Postgres is a relational database. So to simplify things, weâre going to get started with SQLite in this section. SQLite is a very small kind of relational database. It uses SQL. So you can interact with it in a very similar way as MySQL or Postgres. One big advantage of SQLite is that you donât need to have an additional piece of software running. Postgres you first would have to install and always running. And thatâs what you should do in a larger application. But when youâre just starting out, itâs fine to start with SQLite. It stores the entire database in a single file in your project folder.
(Side note on hosting: You wonât be able to use SQLite as the main database on PaaS providers such as Heroku or Google App Engine. As mentioned in earlier exercises, they donât allow files to be written on the server except for the deployment itself. Since SQLite is data stored in a simple file of your project folder, the data wonât be stored long term. So as soon as you want to deploy your application, youâll have to use something like Postgres instead. But weâll talk about that more in a later section.)
Adding SQLAlchemy as Extension to Flask
When working with any database in your program, itâs best if you donât have to write raw SQL code. Instead, you should use an ORM (Object-relational mapping) tool. One very popular tool for SQL databases in Python is SQLAlchemy. ORMs are essentially prewritten Python functions that allow you to interact (read, write, delete, update) with data in any SQL database by writing basic Python code. This way, you can switch databases without changing much of your code. Think of it as an interface to interact with the database by writing basic Python code.
SQLAlchemy is a library, and you can install it by running
pip install flask_sqlalchemy
Donât forget you need to have your virtual environment active before you install the package. Also, donât forget to freeze your requirements.txt file after every package you install.
Now, we can import it. But where? Since we refactored our project to use blueprints and extract code from the app.py file, it wouldnât be a good idea to add it directly in there. Instead, create a new folder called extensions in the app folder and in it add a new file called database.py.
The extensions folder will hold all additions to our applications that, e.g., come from third-party packages. Later, we may, for example, add the authentication extension.
In /app/extensions/database.py, add:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
Similar to Flask
, SQLAlchemy
is a class that needs to be initialized. Thatâs what weâre doing here.
Next, we need to connect the db
variable (representing an instance of the SQLAlchemy
class) with our application.
Back in our /app/app.py file, import the db
variable at the top:
from app.extensions.database import db
At the bottom of the file, create a new function that will behave very similarly to our register_blueprints
function - except itâll register our extensions:
def register_extensions(app: Flask):
db.init_app(app)
db.init_app(app)
initializes the database connection of SQLAlchemy
and passes the app
(which is our Flask application object) as an argument.
Finally, add register_extensions
to the create_app
function but before the register_blueprints
.
def create_app():
app = Flask(__name__)
app.config.from_object('app.config')
register_extensions(app)
register_blueprints(app)
return app
Configure SQLAlchemy
Right now, SQLAlchemy is connected to our application, but there is no database itâs using.
SQLAlchemy will automatically use an in-memory SQLite database if you donât define anything. That means all data is only temporarily stored in memory and removed as soon as the server stops.
We want to create an SQLite database file in our project directory. To do that, you can add another configuration to Flask. Normally, you would do that by writing:
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
But donât do that! Remember, we actually have this line in our project app.config.from_object('app.config')
to load our config file? We can use this file whenever a tutorial tells us to define a configuration with app.config[]
.
But there is another thing. Itâs good practice to store the URL to your database in an environment variable. Thatâs because, most likely, youâll have a different URL depending on the environment your app is in. So, for example, your local development version of the app will use a different database than when itâs deployed to a server.
In your .env file, add the following line:
DATABASE_URL=sqlite:///database.db
Next, back in /app/config.py, add the following import at the top:
from os import environ
os
is a built-in package in Python, and environ
allows us to read environment variables.
Further down in the file, add:
SQLALCHEMY_DATABASE_URI = environ.get('DATABASE_URL')
SQLALCHEMY_DATABASE_URI
is a predefined configuration variable used by SQLAlchemy to know the URL to your database. With the value from the environment variable, we tell SQLAlchemy to use SQLite and store it in a file called database.db.
To keep our project code clean, you should add the name of the database.db file on a new line to the .gitignore file!
Thatâs it. Thatâs all you need to set up a very basic SQLite database.
Setup SQLAlchemy for testing
When it comes to tests, itâs good practice to keep the database used for testing completely separate from the database I use for development or on the server.
So the use of an environment variable comes in very handy here as we can just overwrite it for our tests.
Open the /app/tests/conftest.py file and import environ
at the top:
from os import environ
Inside the fixture and right above app = create_all()
add this line:
environ['DATABASE_URL'] = 'sqlite://'
With this line, we overwrite the environment variable and define it as 'sqlite://'
. This will cause SQLAlchemy to use the default in-memory SQLite database for our tests. Thatâs fine because we donât want our testing data to exist permanently. Itâs fine to be cleared after every test.
Defining a model with SQLAlchemy
Now that SQLAlchemy is all set up, I can use it to define my model. The reason for that is that I want my model to represent a table in my database. The database will have several tables later on. Maybe one for users, one for orders, but also one for cookies. And thatâs the one we want to create now.
Again, the ORM makes it easy to define my data structure by just writing Python code. SQLAlchemy will handle translating this to the actual database - no matter whether itâs SQLite or Postgres.
To define the data structure, I need to inherit my Cookie
class from a class defined in SQLAlchemy called db.Model
- like this: class Cookie(db.Model)
.
In /app/cookies/models.py, first, we need to import the db
variable. Then, we can adjust our Cookie
class to inherit from db.Model
:
from app.extensions.database import db
class Cookie(db.Model):
Now, letâs define the properties we created placeholders for earlier. The way you do that is with the db.Column()
method provided by SQLAlchemy. That method tells SQLAlchemy to create a column for the given property. As a function parameter, you need to tell SQLAlchemy what type of column to create. Some types are db.Integer
, db.String
, db.Date
. Check out this complete list of data types available.
Here is what it would look like doing it for all our properties:
class Cookie(db.Model):
id = db.Column(db.Integer, primary_key=True)
slug = db.Column(db.String(80), unique=True)
name = db.Column(db.String(80))
price = db.Column(db.Numeric(10, 2))
Make those changes to your /app/cookies/models.py file.
There are a few important things to point out.
Each model you define should always have an id
property. And unless you have a good reason to do it differently, it should just be defined as db.Integer
and have the primary_key
parameter set to True
. SQLAlchemy will automatically fill that value for you when new records are created. And each new record will have an incremented number as ID. That means the first record youâll ever create will automatically get the ID 1
, the second one 2
, the third one 3
, and so on. The primary_key
attribute tells SQLAlchemy that this field will be used to identify the record primarily. Therefore, it must be unique. There can never be two records with the same ID in a single table.
The 80
thatâs the argument of the db.String()
function limits the stringâs length to 80 characters. Users will not be able to create or update records to have longer strings. Itâs considered good practice to limit the length for security reasons (for example, so nobody can go nuts and fill your database with insane amounts of data). Keep in mind, though, not to be too restrictive. Especially when you deal with the names of real people, there are plenty of bad examples out there of applications that required a minimum length higher than the length of some peopleâs actual names or limit the street name field so much that the full street name couldnât fit. Just be cautious.
Then, there is a parameter unique=True
. As you might guess, this parameter tells SQLAlchemy that this field value should also always be unique. Since we use the slug
for the URL to our cookies, we donât want any cookies to have the same slug
accidentally. If we try to create a record with the same slug as another one, this will throw an error, and the creation wonât happen.
Finally, we chose the db.Numeric
type for the price. We could have also used db.Integer
or db.Float
. But in the case of prices, both those data types have downsides. Integers have a maximum value, and floats do wonky things once you start doing math with them. And you certainly donât want your app to do funny things when it comes to financial information. The numeric data type uses multiple integers to store a numeric value that may include decimals. The first parameter, 10
, defines how many integers to use. In this case, we chose 10
- although itâs unlikely that weâll ever have a cookie that expensive. The second value, 2
, defines how many decimal points we want to allow since this is a price, we want there only ever to be a maximum of 2 decimal points.
Migrations
Alright! We got our model set up. Now we need to actually let SQLAlchemy create a database table that matches what we defined in the Cookie
model.
This is the part where a lot of other tutorials will tell to create a new database with db.create_all()
in the python3
console. db.create_all()
is a function provided by SQLAlchemy to create all the database tables based on the models you have defined in your application. But itâs not sustainable to always have to do that in a growing application. And itâs not considered best practice either.
Most larger applications use a concept called migrations. A migration is like a script that migrates your Python models to the database as tables. Each time you make a change, a new migration file is created. This way, you can keep track of all the changes that ever happened in your application. It also makes it easier to roll changes back if something goes wrong. Thatâs why Iâd suggest using migrations from the beginning.
Fortunately, there is a useful package for that again. Run pip install flask_migrate
. (Donât forget to freeze the requirements.txt file)
Then, add it to your /app/extensions/database.py file:
from flask_migrate import Migrate
Right below where you initialized db = SQLAlchemy()
, insert:
migrate = Migrate()
Back in /app/app.py add migrate
to the import at the top:
from app.extensions.database import db, migrate
Then, initialize it in the register_extensions
function you created before right below the existing db.init_app(app)
:
migrate.init_app(app, db, compare_type=True)
The first parameter app
tells the migration package where to look for the models. The migration package is configured to automatically scan your Flask application for any models. It will automatically generate a script for you based on the models that are imported in your app. That also means that if you create a model in a .py-file in your project but never import it anywhere, the package will also not find it!
The second parameter, db
, as you might have guessed, tells the script where to find the database that should be dated with the new database tables.
And finally, the parameter compare_type=True
is a configuration option. That means itâs optional and you could leave it out. But I do recommend adding it here. This option will enable any changes you make to column types in your models and allow you to create migrations for it. Letâs say you have a User
model with a name
thatâs configured to be a string of 50 characters. But later you realize thatâs too short and you want to increase the number to 200. Without this option, the migration script wouldnât notice when you make that change. But with this option, you can make the change, then generate a migration file and update the database.
As the final step, because we refactored our app to be started with the run.py file, we need to tell FlaskMigrate where it can find our app. We do that by adding FLASK_APP=run.py
to a new line in our .env file.
Once youâve done that, you can use the command line to create migrations.
Initial database creation
First, you need to initialize the database. Do that by running:
flask db init
To make sure it has worked, you should see a new folder called migrations created in your project folder. That should include a whole bunch of stuff. Among them is a versions folder.
If thatâs not the case, try to identify the error messages you got in the terminal. There may be some warnings. Thatâs fine, and weâll deal with those later. Try to see if there are errors and not warnings. They may give you an idea of what went wrong. Sometimes it can be as simple as a typo in your code or a missing colon.
You only need to run this init
script once in the beginning and then never again.
Making database updates with migrations
Once you get the migrations folder, you can move on to the next step. Now we can create the actual migration for the Cookie
model.
Before we can do that you need to keep in mind one important thing: FlaskMigrate will automatically search your project for models and add changes to those to the migrations. But, for FlaskMigrate to actually find a model, it must be imported as part of your project. Right now, our models.py file exists, but itâs not imported anywhere in our code. So it would simply be ignored by FlaskMigrate. To fix that, open /app/cookies/routes.py and at the top add:
from .models import Cookie
This will import the model. We wonât use it until a later exercise. But this is the file where weâll need it. So we can already import it here.
Now, you can create a new migration with this command in the command line:
flask db migrate -m 'create cookie model'
Execute this command and then have a look in the migrations/versions folder. You should now see a new file was created. This file includes the script that interacts with SQLAlchemy to create a new database table for our Cookie
model.
The text of the string in the end 'create cookie model'
could have been anything you like. Think of those like git commits. They should describe what changes you made. This will help you, later on, more easily keep track of the changes you made over time.
Now that the migration was created, you can actually run the migration script with:
flask db upgrade
If everything went well, you should see something like this in your command line:
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 339d18852676, create cookie model
Alright great! We have created a database table! Our app now doesnât do much with the data we have set up. But you now know how to define models and add new tables to a database.
Adding Migrations to Tests
If you remember from before, we create a blank in-memory database for our tests. So any migrations we run are not applied to the database we want to use for testing.
The way to solve this is to run a migration before our test. Luckily thatâs not too difficult to set up. In /app/tests/conftest.py import upgrade
from flask_migrate
:
from flask_migrate import upgrade
Inside the with app.app_context()
block of our fixture, right before our yield app.test_client()
add the upgrade
function:
with app.app_context():
upgrade()
yield app.test_client()
upgrade
works just like the command line action we used earlier to run our migrations. with app.app_context()
is a requirement of the flask_migrate
library. It makes sure that upgrade()
only runs within the context of our application. Learn more about with
here.
Making changes to models
If you want to add another model or add properties to existing models, all you have to do is add the code and create another migration. So, for example, if you want to add a field picture_url
to the Cookie
, just change it accordingly:
class Cookie(db.Model):
id = db.Column(db.Integer, primary_key=True)
slug = db.Column(db.String(80), unique=True)
name = db.Column(db.String(80))
price = db.Column(db.Numeric(10, 2))
picture_url = db.Column(db.String(260))
Now you just add a new migration and then run it in the command line:
flask db migrate -m 'add picture_url to Cookie'
flask db upgrade
If you want to delete a column, you can just remove the property from the code, then create another migration, and finally upgrade
it again.
Important: Donât change the name of a property or remove and add properties within the same migration. Changing existing column names or removing one column and adding another within the same migration can cause issues with SQLite. This is different from other databases. But SQLite doesnât support just changing existing columns. So what youâd have to do is to use separate migrations: one for removing a column and another for adding a new column. (âď¸ Note that this will mean the data within a deleted column will be lost!)
Troubleshooting Migration Errors
Sometimes things go wrong, and youâll see an error in your command line when running migrations. Pay good attention to the output in your command line to make sure you catch those errors early.
One common error e.g., looks like this: sqlite3.OperationalError: duplicate column name
.
One big benefit of migrations is that they allow you to roll back changes you made more easily. Letâs say you see an error when trying to run flask db upgrade
. What you can do then is to run flask db downgrade
instead.
If you take a look at the different files in the migrations/versions folder, youâll see that each script contains a upgrade
and downgrade
functions. The migrations have a script built-in to allow you to reverse changes easily. By running flask db downgrade
, youâll roll your latest migration to the previously working version. The command line output will show you the specific version with something like this:
INFO [alembic.runtime.migration] Running downgrade ba34b67747d0 -> 339d18852676, add picture_url to Cookie
You can then find the migration file in the migrations/versions folder (in the example, itâs 339d18852676
) to see which version of the database that refers to. The name of the downgraded migration file youâll see before the arrow in the command line (in our example, thatâs ba34b67747d0
). You could find the downgraded file and either remove it if it caused the error. Or you can attempt to fix the specific errors you found.
If you choose just to remove the migration file, you can just delete it, make whatever changes you need to make to your models, and create a new migration file with flask db migrate
and run flask db upgrade
. Hopefully, this time itâs fixed and works without problems.
Using another database
Everything you learned about in this exercise works exactly the same for another SQL database such as Postgres or MySQL. With those, your configuration SQLALCHEMY_DATABASE_URI
will not point to sqlite
. Instead, youâd install the software, e.g., Postgres. Then, youâd set up a database and, in the end, get a URL to that database. That URL you could insert in the configuration instead of 'sqlite:///database.db'
. We will do exactly that in a later exercise and when you have to deploy the application to a server.
đ Practice
- In your separate practice project, install
flask_sqlalchemy
andflask_migrate
and set up both database and migrations with SQLite. Add them as extensions to your app. - Come up with at least one model you need. Create a class for it, make sure it inherits from
db.Model
, and define its columns. - Make sure both SQLAlchemy and FlaskMigrate are set up for testing. This will be important later on.
- Optional: Try adding separate migrations and running them with
flask upgrade
where each migration adds a property to your model or removes one. Make sure you have separate migrations each time. Try running several of these to start getting a feeling for how it works. And donât worry about breaking things. This is a practice application. This is the best situation to break things.