Database migrations are essential for keeping evolving applications aligned with changing data models, without losing consistency or integrity.
In this article, we’ll examine how Flask, SQLAlchemy, and Alembic work together to manage schema changes while ensuring smooth upgrades to your database schema.
Initial Setup
For simplicity, I’ll describe the steps for a project using Flask 3, SQLAlchemy 2 and a SQLite database in the following steps. To integrate Alembic into the project, follow these steps:
- Install Alembic:
|
|
- Initialize Alembic in your project:
|
|
This creates a alembic directory and a alembic.ini file in your project root.
- Update
alembic.iniat the root of the project and find thesqlalchemy.urlline and set it to your SQLite database URL:
|
|
- Modify
alembic/env.pyto use your SQLAlchemy models. Add these lines near the top:
|
|
Note: replace your_app with the name of your Flask application module.
- Create a migration:
|
|
- Apply the migration:
|
|
- For future changes, repeat steps 5 and 6.
Integration With the Flask Application
To integrate this with Flask, you could do the following way:
|
|
With the above, the alembic.ini file, in particular the sqlalchemy.url setting, isn’t used anymore, as the code above takes precedence.
It’s a more flexible manner to configure the database to use when you run your application. This way, you could specify in your configuration a separate sqlalchemy.url depending on your environment.
Note: the configuration object isn’t detailed in the code above, but you picture it as a class with properties that are pulled from a .env file.
Editing the File Generated by Alembic
Let’s say a table is updated with a column and its default requires a value.
Can I edit the migration generated with a custom script using alembic?
You can edit the migration generated by Alembic with a custom script, and it’s often necessary to do so, especially when dealing with non-nullable columns in production.
You can and often should edit the migration script generated by Alembic. After running alembic revision --autogenerate, you’ll find a new script in your alembic/versions directory. You can modify this script to include custom SQL or Python code to handle complex migrations.
Handling New Non-Nullable Columns
Let’s say we want to add non-nullable columns once the table has been in production for a while.
How should we proceed with Alembic’s migration?
It’s generally a good practice to avoid adding non-nullable columns to existing tables in production without careful consideration. This is because:
- If the table already contains data, you need to provide a default value for existing rows.
- Adding a non-nullable column can lock the table during the migration and might cause downtime in a busy production environment.
Here’s a typical approach to safely add a non-nullable column:
- Add the column as nullable first.
- Deploy the change and update the application to start populating the column.
- Once all rows have been populated, alter the column to be non-nullable in a separate migration.
Example of editing a migration script:
|
|
This approach minimizes the risk and potential downtime in production environments.
You can read more about this topic in these articles:
- “Adding a NOT NULL column to an existing table” by PostgreSQL
- “Best Practices for Schema Changes and Data Migrations” by Shlomi Noach
- “Online Schema Change for MySQL” by Facebook Engineering:
These sources provide more detailed information on managing schema changes in production environments and the considerations involved in adding non-nullable columns.
Docker and Alembic Migration
Last but not least, you need to think about Docker when it comes to running your migration to deployed environments.
The above integration with the flask application runs at application startup.
So if you don’t tell in your Dockerfile where the migrations are, it won’t apply them and the application will fail to start.
So make sure to copy the migration in the container:
|
|
Troubleshooting
“Target database isn’t up to date”
Let’s say you modified a table with a new column in the code-first database file, but running alembic revision --autogenerate -m "Add a column to my table X" outputs “Target database isn’t up to date”.
What could be the reason for that message?
The “Target database isn’t up to date” message usually indicates that there are pending migrations that haven’t been applied to your database yet. This can happen if you’ve created migrations previously but haven’t run them, or if there’s a mismatch between your database state and Alembic’s record of applied migrations.
To resolve this issue, try the following steps:
-
Check the current database state:
1alembic currentThis will show you the current revision of your database.
-
View of migration history:
1alembic history --verboseThis will show you all available migrations and their status.
-
If there are pending migrations, apply them:
1alembic upgrade headThis will bring your database up to date with the latest migration.
-
If the above doesn’t work, or if you’re certain that your database should be up to date, you might need to stamp your database with the current head revision:
1alembic stamp headThis tells Alembic that your database is up to date with the latest migration.
-
After doing one of the above, try generating the new migration again:
1alembic revision --autogenerate -m "Description of your changes"
If you’re still having issues, it might be helpful to check:
- The contents of your
alembic_versiontable in the database - The
alembic/versionsdirectory to see what migration scripts exist - Your model definitions to ensure they match the current database state
Conclusion
Here you go! Now you know how to integrate Alembic, a database migration manager to your Flask project.
If you learned something new today, make sure to follow me.
To follow me
Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.
Photo by Ernesto RƎIƎZ.