Managing database migrations and schema changes with Flask and Neon Postgres
Learn how to handle database migrations and schema changes in a Flask application using Flask-Migrate and Neon Postgres
Flask is a lightweight and flexible web framework for Python that makes it easy to build web applications. When working with databases in Flask, SQLAlchemy is a popular choice for an ORM.
As your Flask application grows, so does your database schema and its complexity. Managing these changes effectively is important for maintaining data integrity and smooth deployments.
This guide will walk you through the process of handling database migrations and schema changes in a Flask application using Flask-Migrate and Neon Postgres.
Prerequisites
Before we begin, make sure you have:
- Python 3.7 or later installed
- A Neon account for Postgres hosting
- Basic familiarity with Flask and SQLAlchemy
Setting up the Project
-
Create a new directory for your project and navigate into it:
-
Create a virtual environment and activate it:
This will isolate your project dependencies from other Python projects, so you can manage them independently.
-
Install the required packages:
We are installing Flask, Flask-SQLAlchemy, Flask-Migrate, psycopg2-binary (Postgres driver), and python-dotenv (for managing environment variables).
An additional thing that you might want to do is to create a
requirements.txt
file with the installed packages:This will allow you to easily install the required packages on another machine by running:
-
Create a
.env
file in your project root and add your Neon Postgres connection string:Replace the placeholders with your actual Neon database credentials.
Note that you should never commit your
.env
file to version control. Add it to your.gitignore
file to prevent accidental commits.Instead, you can have a
.env.example
file with the required variables and commit that to your repository. Then, each developer can create their own.env
file based on the.env.example
template including their own credentials.
Creating the Flask Application
With the project set up, let's create the Flask application and set up database migrations.
Create a new file called app.py
with the following content:
This sets up a basic Flask application with SQLAlchemy and Flask-Migrate, and defines a simple User model. Here's a breakdown of what each part does:
-
Imports: We import necessary modules including Flask, SQLAlchemy, Flask-Migrate, os (for environment variables), and dotenv (for loading .env files).
-
Environment Variables: We use
load_dotenv()
to load environment variables from a .env file, which will include our database URL. -
Flask App Initialization: We create a Flask application instance.
-
Database Configuration: We configure the SQLAlchemy database URI using the
DATABASE_URL
environment variable. -
SQLAlchemy and Flask-Migrate Setup: We initialize SQLAlchemy and Flask-Migrate with our Flask app. This sets up our ORM and migration capabilities.
-
User Model: We define a
User
model that represents the structure of ouruser
table in the database. It includes:- An
id
field as the primary key - A
name
field that's required and has a maximum length of 100 characters - An
email
field that's required, unique, and has a maximum length of 120 characters - A
__repr__
method that provides a string representation of the User object
- An
-
Application Run: Finally, we include a conditional to run the application in debug mode if the script is executed directly.
This setup provides a foundation for building a Flask application with database integration and migration capabilities. The User
model can be expanded or additional models can be added as the application grows.
Initializing Migrations
To start using Flask-Migrate, you need to initialize it in your project. Run the following command in your terminal:
This creates a migrations
directory in your project that will store migration scripts. It also generates a migrations/alembic.ini
file that contains the configuration for Alembic, the migration engine used by Flask-Migrate.
Make sure to add the migrations
directory to your Git repository so that you can track changes to your database schema over time.
Creating the Initial Migration
Now, let's create our first migration to set up the initial database schema. Run the following command:
This command generates a new migration script in the migrations/versions
directory and the -m
flag allows you to provide a message describing the migration. The message is useful for tracking changes and understanding the purpose of each migration.
Open the generated file and review the changes. It should contain the SQL to create the user
table based on our User
model. This is possible because Flask-Migrate uses SQLAlchemy's reflection capabilities to generate the migration script based on the model definitions instead of writing raw SQL or manually creating the schema.
It is a good practice to review the generated migration script before applying it to your database. This way, you can ensure that the changes are correct and will not cause any issues as in some cases Alembic might not generate the migration script as expected and you might need to modify it manually, so remember to always review the generated migration scripts.
Applying the Migration
After reviewing the migration script, to apply the migration and create the table in your Neon Postgres database, run:
This command executes the migration script and creates the user
table in your database.
The output that you should see after running the flask db upgrade
command should look something like this:
This indicates that the migration was successful and the user
table was created in your database.
Making Schema Changes
Now that the initial migration is complete, let's make some changes to the schema. We'll add a new field to the User
model and create a new migration to apply the changes.
Let's modify our User
model to add a new field. Update the User
class in app.py
:
With the new age
field added to the User
model, we need to create a new migration to apply this change.
Alembic will generate a new migration script that includes the necessary SQL to add the age
column to the user
table.
Review the generated migration script, then apply it:
The age
column should now be added to the user
table in your database. If you want to revert the changes, you can run flask db downgrade
to roll back the last migration.
If you were to check the user
table in your database, you should see that the age
column has been added:
This will return the data from the user
table including the new age
column.
Renaming Columns
As your application evolves, you may need to rename columns in your database schema.
Such changes need to be handled carefully to avoid data loss or corruption. You also need to make sure that the application code is updated to reflect the new column names otherwise it might not be backwards compatible leading to issues.
To rename a column, you'll need to use SQLAlchemy's alter_column
operation. Let's rename the age
column to years_old
:
- Create a new migration:
- As the
alter_column
operation is not directly supported by Flask-Migrate, you'll need to modify the generated migration script manually.
Open the generated migration file and modify it:
- Apply the migration:
The age
column should now be renamed to years_old
in your database. Remember to be cautious when renaming columns, as it can have implications on your application code and queries.
Working with Indexes
Adding indexes can improve query performance. To learn more about indexing, refer to the Neon documentation.
Let's add an index to the email
column.
Open the User
model in app.py
and add the index=True
parameter to the email
column:
Create a new migration with a descriptive migration message:
Review the generated migration script, it should contain an op.create_index
operation for the email
column:
Finally, apply the newly created migration:
The email
column should now have an index in your database, which can improve query performance when searching by email.
Migrations in CI/CD Pipeline
Automating database migrations in your Continuous Integration pipeline can help with catching potential issues early.
By using Neon's branching feature, you can test your migrations safely without affecting your production database while ensuring that your application code and database schema changes are always in sync.
Here's an example of how you can automate migration testing using GitHub Actions and Neon branches:
This workflow does the following:
-
Fetches the latest code from the pull request.
-
Installs the specified Python version.
-
Installs the required Python packages listed in
requirements.txt
. -
Uses the official Neon GitHub action to create a new branch in your Neon project. This allows you to test migrations in isolation.
-
Applies any pending database migrations to the newly created Neon branch.
-
Executes your test suite against the updated database schema in the Neon branch.
-
Deletes the temporary Neon branch after the workflow completes, regardless of success or failure to make sure that no resources are left behind.
Using Neon's branching feature in your CI pipeline offers several advantages:
- You can test your migrations and schema changes in a separate branch without affecting your production or staging databases.
- Catch migration issues before they reach your main branch or production environment.
- Ensures that your database schema changes are always tested alongside your application code changes.
- Allows you to run your full test suite against the updated schema without risk to existing data.
Conclusion
Managing database migrations is an important part of maintaining and evolving your Flask application. With Flask-Migrate and Neon Postgres, you have powerful tools at your disposal to handle schema changes efficiently and safely. Remember to always test your migrations thoroughly and have a solid backup strategy in place.
One thing that you should get in the habit of doing is to always review the generated migration scripts before applying them to your database. This way you can ensure that the changes that are about to be applied are correct and that they will not cause any issues. As well as that, you should use meaningful names for your migrations so that you can easily identify what each migration does.