Working with database

Working with databases in FastAPI involves using ORMs (Object-Relational Mappers) like SQLAlchemy, which allows you to interact with your database using Python objects. FastAPI doesn't come with a built-in database handling solution, but it integrates smoothly with SQLAlchemy for this purpose. Here's a comprehensive guide on how to set up and use databases with FastAPI.

Step-by-Step Guide to Using Databases in FastAPI

1. Install Required Packages

You will need SQLAlchemy, along with an appropriate database driver. For this guide, we'll use SQLite, which is simple and easy for demonstration purposes.

pip install fastapi sqlalchemy databases[sqlite] alembic
  • fastapi: The web framework.
  • sqlalchemy: The ORM for database interactions.
  • databases: Async database support for FastAPI.
  • alembic: For database migrations (optional but recommended).

2. Set Up the Project Structure

Create a project structure as follows:

fastapi_database/
│
├── alembic/
├── main.py
├── models.py
├── schemas.py
├── crud.py
├── database.py
├── migrations/
│   ├── env.py
│   └── versions/
└── alembic.ini
  • main.py: The FastAPI application.
  • models.py: SQLAlchemy models.
  • schemas.py: Pydantic models (schemas).
  • crud.py: CRUD operations (Create, Read, Update, Delete).
  • database.py: Database configuration and session management.
  • alembic/: Directory for Alembic configurations.
  • migrations/: Directory for Alembic migration scripts.

3. Database Configuration

Set up database.py to handle the database connection and session management.

# database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"  # Using SQLite for simplicity

# For PostgreSQL, use: 
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}  # Needed for SQLite
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
  • create_engine: Creates the engine to connect to the database.
  • SessionLocal: A session factory for managing database sessions.
  • Base: Base class for all ORM models.

4. Define SQLAlchemy Models

Create models.py to define your database models.

# models.py
from sqlalchemy import Column, Integer, String, Boolean
from .database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    email = Column(String, unique=True, index=True)
    full_name = Column(String)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)
  • This example defines a User model with several fields.

5. Pydantic Schemas

Create schemas.py to define the data validation and serialization schemas.

# schemas.py
from pydantic import BaseModel

class UserBase(BaseModel):
    username: str
    email: str
    full_name: str | None = None
    is_active: bool | None = None

class UserCreate(UserBase):
    password: str

class User(UserBase):
    id: int

    class Config:
        orm_mode = True
  • UserBase: Base schema for common user fields.
  • UserCreate: Schema for user creation, including the password.
  • User: Schema for representing a user, including the ID.

6. CRUD Operations

Create crud.py to define functions for CRUD operations.

# crud.py
from sqlalchemy.orm import Session
from . import models, schemas

def get_user(db: Session, user_id: int):
    return db.query(models.User).filter(models.User.id == user_id).first()

def get_user_by_email(db: Session, email: str):
    return db.query(models.User).filter(models.User.email == email).first()

def get_users(db: Session, skip: int = 0, limit: int = 10):
    return db.query(models.User).offset(skip).limit(limit).all()

def create_user(db: Session, user: schemas.UserCreate):
    fake_hashed_password = user.password + "notreallyhashed"  # Replace with a real hashing mechanism
    db_user = models.User(
        username=user.username, email=user.email, full_name=user.full_name, hashed_password=fake_hashed_password
    )
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user
  • These functions interact with the database using SQLAlchemy queries.
  • In a real application, ensure to use a proper password hashing mechanism like bcrypt.

7. Create API Endpoints

In main.py, set up the FastAPI application and endpoints.

# main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

from . import crud, models, schemas, database

models.Base.metadata.create_all(bind=database.engine)  # Create tables

app = FastAPI()

@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(database.get_db)):
    db_user = crud.get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(status_code=400, detail="Email already registered")
    return crud.create_user(db=db, user=user)

@app.get("/users/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 10, db: Session = Depends(database.get_db)):
    users = crud.get_users(db, skip=skip, limit=limit)
    return users

@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(database.get_db)):
    db_user = crud.get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user
  • /users/: POST endpoint to create a new user.
  • /users/: GET endpoint to list users with pagination.
  • /users/{user_id}: GET endpoint to retrieve a user by ID.

8. Running the Application

Start your FastAPI application using Uvicorn:

uvicorn main:app --reload
  • Open your browser and navigate to http://localhost:8000/docs to access the interactive Swagger UI.
  • Use the endpoints to interact with your database.

9. Handling Database Migrations with Alembic

Alembic is a lightweight database migration tool for SQLAlchemy. It's used to handle schema changes over time in a controlled and versioned manner.

Initial Alembic Setup

Initialize Alembic:

alembic init alembic

This creates the alembic directory and configuration files.

Edit alembic.ini to set your database connection URL:

# alembic.ini
sqlalchemy.url = sqlite:///./test.db  # Update this with your database URL

Edit alembic/env.py to import your metadata:

# alembic/env.py

from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

# 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.
fileConfig(config.config_file_name)

from myproject.database import Base  # Import your Base

# add your model's MetaData object here
# for 'autogenerate' support
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():
    """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 run_migrations_online():
    """Run migrations in 'online' mode.
    In this scenario we need to create an Engine
    and associate a connection with the context.
    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",


        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(connection=connection, target_metadata=target_metadata)

        with context.begin_transaction():
            context.run_migrations()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

Generate the initial migration script:

alembic revision --autogenerate -m "Initial migration"

Apply the migration:

alembic upgrade head

This creates the initial tables in your database.

Key Concepts

  1. SQLAlchemy ORM: Provides a way to define your database schema as Python classes and interact with the database using Python objects.
  2. Pydantic: Used for defining request and response schemas and performing data validation.
  3. Database Sessions: Managed using SQLAlchemy's sessionmaker, allowing for controlled interactions with the database.
  4. CRUD Operations: Standard operations for creating, reading, updating, and deleting database records.
  5. Database Migrations: Alembic manages schema changes over time, enabling easy upgrades and downgrades of the database schema.

Conclusion

This guide provides a solid foundation for integrating FastAPI with a database using SQLAlchemy and managing your database schema with Alembic. By following these steps, you can build scalable and maintainable API applications with a robust data layer.

Further Reading

By mastering these components, you can efficiently manage data in your FastAPI applications.