FastAPI - SQLAlchemy and Alembic Integration

Almost in every project we store information in databases. It could be an SQL or a NoSQL database. Let's say you would like to go with an SQL database. You have plenty of options to choose. In SQL databases you need to define the structure such as columns and its data types, limitations, default values etc. You may want to create indexes, views and transactions as well. SQLAlchemy allows you to define your tables in form of python classes. You can get the value of a column just by getting the attribute of class. It helps you to get advantage of OOP paradigm. ORM is capable of creating the tables but it's not preferred way for production workloads. It's not a sustainable option. When you modify your structure, it can't make it happen. It's not good as well. Because you don't track what changes you made before. In this point alembic comes to your help. It's capable of creating scripts to modify your structure. It provides rollback scripts as well.

Install required packages

poetry add sqlalchemy alembic
fooapi/ # Main package
    fooapi/
    	__init__.py
        main.py
    tests/
    poetry.lock
    pyproject.toml
    db/
    	__init__.py
        base_class.py
        base.py
        session.py

SQLAlchemy

We should create a new sub-package called db. We store the base classes, session objects and configurations for database connection. The structure is like below.

We define our base class first. We inherit new model classes from the base class. We can define the common columns in base class. It effects every model classes that is inherited from it. So that you don't have to type commun columns in each model classes.

from sqlalchemy.orm import as_declarative
from sqlalchemy import Column, Integer

class_registry: dict = {}

@as_declarative(class_registry=class_registry)
class Base:
	id = Column('id', Integer, primary_key=True, index=True)

class_registry dictionary is used for tracking the which classes are used creating database tables. It's mapping. as_declarative means that we declare the attributes and structure of table. We added an id column which is common for all of our tables.

Now we just defined our Base class. We need to initiate a Session to connect the database.  SQLAlchemy is able to connect to the different database engines with almost same code or with small changes.
We keep database connection string in a variable. We need to create an engine. ORM gets the target engine from connection string. We can pass some extra parameters here. For instance, sqlite doesn't support multi-thread applications. FastAPI creates threads for requests because it's based on starlette (ASGI) which works async. We define that don't check is the connection coming from same thread, or not.
sessionmaker functions returns Session object which is used actually to connect and initiate a session in database. This object is callable. If you want to connect to the db, you call this object. SessionLocal()

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine

SQL_URI = "sqlite:///app.db"

engine = create_engine(SQL_URI, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(engine, autoflush=False, autocommit=False)

We create another module called base.py I'll explain that in alembic section. This is how it looks like.

from .base_class import Base

Alembic

We need to initate alembic in project. Go to directory next to pyproject.toml and run alembic init alembic It creates a directory named alembic and alembic.ini file next to pyproject.toml This is last directory setup.

fooapi/ # Main package
    fooapi/
    	__init__.py
        main.py
    tests/
    poetry.lock
    pyproject.toml
    db/
    	__init__.py
        base_class.py
        base.py
        session.py
    # NEW files below
    alembic.ini 
    alembic/
    	versions/
        env.py

Find and comment out sqlalchemy.url line in alembic.ini, because we set this value by getting from session.py Make below changes in env.py

from fooapi.db.base import Base
target_metadata = Base.metadata

from fooapi.db.session import SQL_URI
def get_url() -> str:
	return SQL_URI

target_metadata is required to run alembic properly. It finds Base class get its metadata with table-class mapping that we defined in base_class.py
We imported SQL_URI to point out the target database.

In addition to that, we must change the run_migrations_offline function. We commented out the sqlalchemy.url in alembic.ini. Update the value of url parameter in context.configure method.

context.configure(
    url=get_url,
    target_metadata=target_metadata,
    literal_binds=True,
    dialect_opts={"paramstyle": "named"},
)

We have to make some updates in run_migrations_online function as well. Because we load connection string from session.py We need to set the connection string in this configuration too.

configuration = config.get_section(config.config_ini_section, {})
configuration['sqlalchemy.url'] = get_url()
connectable = engine_from_config(
    configuration,
    prefix="sqlalchemy.",
    poolclass=pool.NullPool,
    )

Alembic Revision and Apply

Alembic creates migration scripts and they're versioned. To create a new revision and apply it, you can give this command

alembic revision --autogenerate -m "first revision"
alembic upgrade head

Most probably it's gonna create anything in database, because we didn't define any model classes that are mapped to real database tables. At least we tested our integration in this step.

Create DB Model

Create a new sub-package called models under the fooapi package. Create a new module named user.py We define the user model class in this module.
It must be inherited from Base class. We have only 2 columns for this table. We have to set the table name as well.

from fooapi.db.base_class import Base
from sqlalchemy import Column, String


class User(Base):
    __tablename__ = "users"
    
    username = Column(String, index=True)
    password = Column(String)

Now go to the base.py under the db sub-package, import this model class, otherwise alembic isn't going to detect that model class is available.

from .base_class import Base

from fooapi.models.user import User

This part is really important. You must have base.py and base_class.py both.
Alembic needs base.py. Model classes need base_class.py.
Alembic needs Model classes as well, that's why we import model classes in base.py
If you didn't define your Base class in base_class.py you would cause circular dependency. That's why you must define this in separated files. If you get an error about that, ensure you imported Base class from the correct module.

Model Classes;
- Import Base class from base_class.py
- Import Module class within base.py for alembic to discover it.

Alembic;
- Import base.py
- base.py must import Base class from base_class.py
- Make sure you imported all defined Model classes within base.py

To be able to create the table for User model. Give below commands

alembic revision --autogenerate -m "User model added"
alembic upgrade head 


alembic downgrade -1  # To revert changes

Now we have created a table by assist of Alembic. We store the changes in our repository and able to revert and provision versions in database with less-effort.