
Introduction
Managing databases across various environments—like development, testing, QA, and production—can become quite a headache in software development. Each environment often needs its own database setup, and keeping them all in sync can be challenging. But don’t worry, we’ve got a solution!
In this blog post, we'll walk you through a straightforward approach to handling multiple databases in a FastAPI project using SQLAlchemy for ORM and Alembic for migrations. By the end, you'll know how to configure your project so you can easily create and apply migrations to any number of databases with just a few commands. Let's dive in and simplify your database management process!
Prerequisites
Before we begin, make sure you have the following installed:
- Python 3.7+
- FastAPI
- SQLAlchemy
- Alembic
- MySQL (or any other database you prefer)
aiomysqllibrary for asynchronous MySQL connectionspython-dotenvfor environment variable management
You should also have a basic understanding of these tools and how to create a FastAPI project.
Setting Up Multiple Databases
First, let's define our database configuration. We'll use environment variables to manage database credentials and connection URLs.
To deploy and manage your MySQL databases using Docker, follow these steps:
Run Container
First, run a MySQL container:
docker run --name mysqldb -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_USER=dev -e MYSQL_PASSWORD=root -e MYSQL_DATABASE=dev mysql:latestLogin into Container
Access the MySQL shell within the running container:
docker exec -it mysqldb /bin/bash
mysql -u root -p
Enter password: Create Databases
Once inside the MySQL shell, create the additional databases:
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| dev |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.02 sec)
mysql> create database test;
Query OK, 1 row affected (0.03 sec)
mysql> create database qa;
Query OK, 1 row affected (0.02 sec)
mysql> create database prod;
Query OK, 1 row affected (0.00 sec)
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| dev |
| information_schema |
| mysql |
| performance_schema |
| prod |
| qa |
| sys |
| test |
+--------------------+
8 rows in set (0.01 sec)Create a .env file in your project root with the following content:
DB_USER=username
DB_PASSWORD=password
DB_HOST=localhost
DB_PORT=3306Install Dependencies
pip install alembic sqlalchemy fastapi aiomysql python-dotenvConfiguring Alembic
Initiaiize alembic:
alembic init alembicThe alembic/env.py file is where we configure Alembic to handle our multiple databases. We'll set up both offline and online migration contexts.
Here's the env.py configuration:
#!/usr/bin/env python
# alembic/env.py
import asyncio
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import AsyncEngine
from alembic import context
from app.database.base import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
configuration = config.get_section(config.config_ini_section)
configuration["sqlalchemy.url"] = config.get_main_option("sqlalchemy.url")
connectable = AsyncEngine(
engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True,
)
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())
Alembic CLI Script
We will use a custom CLI script (alembic_cli.py) to handle migrations for our different environments. This script allows us to create revisions and apply upgrades across multiple databases.
Here's the script for reference:
# alemibic_cli.py is a script that allows you to run Alembic migrations for multiple databases.
from datetime import datetime
import os
import argparse
from alembic.config import Config
from alembic import command
from dotenv import load_dotenv
load_dotenv()
# Define databases
DATABASES = {'dev', 'test', 'qa', 'prod'}
print(DATABASES)
def generate_db_url(db_name):
return f"mysql+aiomysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{db_name}"
def create_revision(db_name, config_path, message):
"""Create a new revision for a specific db_name."""
versions_dir = os.path.join("alembic", "versions", db_name)
os.makedirs(versions_dir, exist_ok=True)
config = Config(config_path)
config.set_main_option("version_locations", versions_dir)
config.set_main_option(
"sqlalchemy.url",
generate_db_url(db_name),
)
print(f"revision for {db_name}: {config.get_main_option('sqlalchemy.url')}")
command.revision(
config, message=f"{message} on {datetime.now()}", autogenerate=True
)
def upgrade_head(db_name, config_path):
"""Upgrade to head for a specific db_name."""
config = Config(config_path)
config.set_main_option(
"sqlalchemy.url",
generate_db_url(db_name),
)
config.set_main_option(
"version_locations", os.path.join("alembic", "versions", db_name)
)
print(f"upgrade for {db_name}: {config.get_main_option('sqlalchemy.url')}")
command.upgrade(config, "head")
def downgrade(db_name, revision, config_path):
"""Downgrade to a specific revision for a customer."""
config = Config(config_path)
config.set_main_option('sqlalchemy.url', generate_db_url(db_name))
config.set_main_option('version_locations', os.path.join('alembic', 'versions', db_name))
print(f"downgrade for {db_name}: {config.get_main_option('sqlalchemy.url')}")
command.downgrade(config=config, revision=revision)
def migrate_database(db_name, action, config_path, message, revision=None):
"""Run migration for a specific db_name."""
if action == "revision":
create_revision(db_name, config_path, message)
elif action == "upgrade":
upgrade_head(db_name, config_path)
elif action == 'downgrade' and revision:
downgrade(db_name, revision, config_path)
else:
print(f"Error: Invalid action or missing revision for downgrade.")
def migrate_all(action, config_path, message):
"""Run migrations for all db_names."""
for db_name in DATABASES:
migrate_database(db_name, action, config_path, message)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Run Alembic migrations for db_names."
)
parser.add_argument(
"action",
choices=["revision", "upgrade", "downgrade"],
help="Action to perform: 'revision' or 'upgrade', or 'downgrade'",
)
parser.add_argument(
"--db", help="Specify a db_name to run the migration for"
)
parser.add_argument(
"--message", help="Revision message for the migration", default="rev"
)
parser.add_argument('--revision', help="Specify the revision for downgrade (required for 'downgrade' action)")
args = parser.parse_args()
config_path = "./alembic.ini"
if args.action == 'downgrade' and not args.revision:
print("Error: Missing revision for downgrade.")
exit(1)
if args.db:
if args.db in DATABASES:
migrate_database(args.db, args.action, config_path, args.message, args.revision)
else:
print(
f"Error: Database '{args.db}' not found in the list of databases."
)
else:
migrate_all(args.action, config_path, args.message)
Usage
The script supports three primary actions: revision, upgrade, and downgrade.
General command:
python alembic_cli.py <action> [--db <db_name>] [--message <message>] [--revision <revision>]
Actions
- revision: Create a new migration revision.
- upgrade: Upgrade the database schema to the latest revision.
- downgrade: Downgrade the database schema to a specific revision.
Options
--db <db_name>: Specify a specific database (e.g.,dev,test,qa,prod). If omitted, the action will be applied to all databases.--message <message>: Specify a message for the revision (only forrevisionaction). Defaults to "rev".--revision <revision>: Specify the revision to downgrade to (required fordowngradeaction).
Examples:
Create a New Revision
Create a new revision for all databases with a message "initial":
python alembic_cli.py revision --message "initial"Create a new revision for the dev database with a message "initial":
python alembic_cli.py revision --db dev --message "initial"Upgrade Database Schema
Upgrade all databases to the latest schema:
python alembic_cli.py upgradeUpgrade the prod database to the latest schema:
python alembic_cli.py upgrade --db prodDowngrade Database Schema
Downgrade the test database to a specific revision:
python alembic_cli.py downgrade --db test --revision <revision>Conclusion
Managing multiple databases can be complex, but with the right tools and configuration, it becomes much more manageable. In this post, we've seen how to set up FastAPI with SQLAlchemy and Alembic to handle migrations across different environments. By using environment variables and a custom CLI script, we can efficiently create and apply migrations for all our databases. This setup ensures that our database schemas remain consistent and up-to-date across all environments.
Feel free to explore further and adapt this setup to your specific needs. Happy coding!
References
GitHub Repo for the code: https://github.com/ktechhub/alembic_multidb
Follow Us on:
GitHub: https://github.com/ktechhub
X: https://twitter.com/ktechhub
LinkedIn: https://www.linkedin.com/company/ktechhub

Experienced DevOps Engineer proficient in Python, Docker, Kubernetes, CI/CD, and cloud technologies. Specialized in automating processes and building scalable infrastructure for seamless development and deployment.


