Flask Migrations
When developing web applications with Flask and a database, you'll inevitably need to modify your database schema as your application evolves. Adding columns, changing data types, or creating new tables manually becomes tedious and error-prone, especially in production environments. This is where database migrations come into play.
Introduction to Database Migrations
Database migrations are a controlled way to evolve your database schema over time. They allow you to:
- Track changes to your database schema in version control
- Apply schema changes consistently across different environments
- Roll back changes if something goes wrong
- Collaborate effectively with other developers
Flask-Migrate is an extension that provides migration support for Flask applications using SQLAlchemy. It's built on top of Alembic, a database migration tool for SQLAlchemy.
Setting Up Flask-Migrate
Installation
First, you need to install Flask-Migrate:
pip install Flask-Migrate
Basic Setup
Here's how to set up Flask-Migrate in your Flask application:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# Define your models
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f'<User {self.username}>'
Migration Commands
Flask-Migrate adds a set of migration commands to Flask's command-line interface. Here are the most important ones:
Initialize Migrations
To start using migrations, you need to initialize the migrations directory:
flask db init
This creates a migrations
folder in your project with the necessary files to track and manage migrations.
Create a Migration
After initializing or when you need to make changes to your models, generate a migration script:
flask db migrate -m "Initial migration"
This command:
- Examines the differences between your database models and the current database schema
- Creates a new migration script in the
migrations/versions
folder with the changes needed
Apply Migrations
To apply the pending migrations to the database:
flask db upgrade
This executes all migration scripts that haven't been run yet.
Rolling Back
If you need to undo the last migration:
flask db downgrade
Real-World Example: Evolving a Blog Application
Let's go through a practical example of using migrations in a simple blog application.
Initial Setup
# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# Initial model
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
@app.route('/')
def index():
return 'Blog Home Page'
if __name__ == '__main__':
app.run(debug=True)
Initialize migrations and create the first migration:
flask db init
flask db migrate -m "Create Post model"
flask db upgrade
Adding a Category Relationship
Now, let's say we want to add categories to our blog posts:
# Updated models
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
# New columns
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
category = db.relationship('Category', backref=db.backref('posts', lazy=True))
Generate a new migration:
flask db migrate -m "Add category relationship"
The migration script will contain the changes needed:
# migrations/versions/xxxxxxxxxxxx_add_category_relationship.py
"""Add category relationship
Revision ID: xxxxxxxxxxxx
Revises: xxxxxxxxxxxx
Create Date: 2023-05-20 12:34:56.789012
"""
# ... Alembic imports ...
def upgrade():
# Create category table
op.create_table('category',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=False),
sa.PrimaryKeyConstraint('id')
)
# Add category_id to post
op.add_column('post', sa.Column('category_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'post', 'category', ['category_id'], ['id'])
def downgrade():
# Remove foreign key
op.drop_constraint(None, 'post', type_='foreignkey')
# Remove category_id column
op.drop_column('post', 'category_id')
# Drop category table
op.drop_table('category')
Apply the migration:
flask db upgrade
Adding Publication Date
Later, we decide to add a publication date to posts:
from datetime import datetime
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
category = db.relationship('Category', backref=db.backref('posts', lazy=True))
# New column
pub_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
Generate and apply the migration:
flask db migrate -m "Add publication date"
flask db upgrade
Best Practices
-
Always review migration scripts: Alembic generates migration scripts automatically, but it's important to review them before applying changes to ensure they do what you expect.
-
Commit migrations to version control: Migration scripts should be part of your version control system to ensure all developers and environments use the same migrations.
-
Test migrations: Before applying migrations to a production database, test them in a development or staging environment.
-
Don't modify existing migrations: Once a migration has been applied and shared, create new migrations for additional changes instead of modifying existing ones.
-
Handle data migrations separately: For complex data transformations, you might need to write custom scripts to handle data migration alongside schema changes.
Advanced Migration Techniques
Custom Migration Scripts
Sometimes you need to perform operations that can't be detected automatically. You can write custom migration scripts:
flask db revision -m "Custom migration"
This creates an empty migration script that you can edit:
def upgrade():
# Your custom migration logic here
op.execute("UPDATE users SET status = 'active' WHERE status IS NULL")
def downgrade():
# Logic to reverse the upgrade
op.execute("UPDATE users SET status = NULL WHERE status = 'active'")
Multiple Database Support
Flask-Migrate supports multiple databases:
app = Flask(__name__)
app.config['SQLALCHEMY_BINDS'] = {
'users': 'sqlite:///users.db',
'posts': 'sqlite:///posts.db'
}
db = SQLAlchemy(app)
migrate = Migrate(app, db)
To run migrations for a specific database:
flask db migrate --multidb
Troubleshooting Common Issues
Head Mismatch
If you get a "Target database is not up to date" error, it means there's a mismatch between the current and head revisions. This often happens when migrations are applied manually or out of order. Fix it by:
flask db stamp head # Mark the current database as up to date
Autogenerate Not Detecting Changes
Sometimes Alembic doesn't detect all changes. Common cases include:
- Changes to constraints or indexes
- Table or column name changes
- Changes to server defaults
For these cases, you may need to write manual migration scripts.
Migration Error Recovery
If a migration fails halfway through:
- Fix the issue that caused the failure
- Run
flask db upgrade
again - If needed, manually fix data inconsistencies
Summary
Flask-Migrate is an essential tool for managing database schema changes in Flask applications. It allows you to:
- Track and version control database changes
- Safely apply schema modifications
- Roll back problematic changes
- Maintain consistency across development, testing, and production environments
By following good practices with database migrations, you can evolve your database schema with confidence, even in complex applications.
Additional Resources
Exercises
-
Create a Flask application with SQLAlchemy models for a simple e-commerce system (products, orders, customers) and set up Flask-Migrate.
-
Add a new field to an existing model and create a migration for it.
-
Create a migration that adds a new model and relates it to existing models.
-
Write a custom migration script that populates a new column with data derived from existing columns.
-
Simulate a failed migration and practice recovering from it.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)