Connecting to Relational Databases with SQLAlchemy
In previous chapters, we learned how to receive API requests and validate data using Pydantic. However, if we shut down the server, this data would disappear. To build a real-world business application, we must store data permanently in a database.
While you could directly write raw SQL statements (like INSERT INTO users...) to interact with the database, in modern backend development, we strongly recommend using ORM (Object-Relational Mapping) technology.
In the Python ecosystem, the most established, robust, and industry-standard ORM is SQLAlchemy.
1. What is ORM? Why Use It?
The core concept of ORM is to map database "tables" to Python "classes."
- Instead of writing
SELECT * FROM users, you simply writedb.query(User).all(). - Instead of writing
INSERT INTO, you just create aUser(name="Ken")object anddb.add()it.
This approach offers three major benefits:
- Prevents SQL Injection Attacks: ORM automatically filters dangerous characters at the underlying level.
- Easy Database Switching: Test with SQLite today and switch to PostgreSQL tomorrow—no need to change any logic code, just the connection string.
- Code Autocompletion: Typing
user.in your editor will automatically suggest all fields, a feature not available when writing raw SQL.
2. Environment Setup and Database Connection
First, install SQLAlchemy and the PostgreSQL driver:
pip install sqlalchemy psycopg2-binary
Next, create a database.py file to configure the database connection:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
# Replace with your own database URL (format: postgresql://username:password@host:port/database)
# Here, we use SQLite for lightweight local testing
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# Create the engine (SQLite requires check_same_thread=False)
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
# Create a Session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create the base class for all Models
Base = declarative_base()
3. Defining SQLAlchemy Models
In models.py, we define the structure of the database tables. Note that this is different from the Pydantic models in the previous chapter. Pydantic validates incoming JSON, while SQLAlchemy defines the structure stored in the database.
from sqlalchemy import Boolean, Column, Integer, String
from database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
4. Dependency Injection Setup
FastAPI has a powerful feature called "Dependency Injection."
Every time an API is called, we need to open a database connection (Session) and close it when the API finishes. We don’t need to manually write this logic in every API.
Add this to main.py:
from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session
import models
from database import SessionLocal, engine
# Let SQLAlchemy automatically create any missing tables
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# Define a Dependency to provide the Database Session
def get_db():
db = SessionLocal()
try:
yield db # Pass the db to the API route
finally:
db.close() # Ensure the connection is closed after the API finishes
5. Hands-on: Writing Data to the Database
Now, let’s combine Pydantic, SQLAlchemy, and Dependency Injection!
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
# Import what we’ve written earlier
from database import get_db
import models
from pydantic import BaseModel
app = FastAPI()
# Define the Pydantic Schema for input
class UserCreate(BaseModel):
email: str
password: str
# Define the Pydantic Schema for output (hiding the password)
class UserResponse(BaseModel):
id: int
email: str
is_active: bool
class Config:
from_attributes = True # Allows direct conversion from SQLAlchemy objects
@app.post("/users/", response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
# 1. Check if the email is already registered
db_user = db.query(models.User).filter(models.User.email == user.email).first()
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
# 2. Create the SQLAlchemy ORM object (in practice, passwords should be hashed!)
fake_hashed_password = user.password + "notreallyhashed"
new_user = models.User(email=user.email, hashed_password=fake_hashed_password)
# 3. Write to the database
db.add(new_user)
db.commit()
db.refresh(new_user) # Retrieve the auto-generated ID
# 4. Return the response to the frontend
return new_user
Summary
You’ve now learned the three pillars of backend development:
- Routing: Handling HTTP requests.
- Validation (Pydantic): Ensuring correct data formats.
- Storage (SQLAlchemy): Safely interacting with the database.
However, in the example above, passwords are stored in plain text, and anyone can call this API. In the next chapter, we’ll learn the core of security: Password Hashing and JWT Authentication!
Database Models
SQLAlchemy models define the schema of your database tables as Python classes.
Declarative Base
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import declarative_base, relationship
from datetime import datetime
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, index=True)
email = Column(String(100), unique=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationship
posts = relationship("Post", back_populates="author")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
content = Column(String(5000), nullable=False)
published = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
author_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", back_populates="posts")
Database Session Management
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
DATABASE_URL = "postgresql://user:password@localhost:5432/mydb"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Dependency for FastAPI routes
def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
CRUD Operations
Create
@app.post("/users/", response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
# Check if username exists
existing = db.query(User).filter(User.username == user.username).first()
if existing:
raise HTTPException(status_code=400, detail="Username already exists")
# Create new user
db_user = User(
username=user.username,
email=user.email,
hashed_password=hash_password(user.password)
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
Read
@app.get("/users/{user_id}", response_model=UserResponse)
def read_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.get("/users/", response_model=list[UserResponse])
def list_users(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
users = db.query(User).offset(skip).limit(limit).all()
return users
Update
@app.put("/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user: UserUpdate, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.id == user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
update_data = user.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(db_user, key, value)
db.commit()
db.refresh(db_user)
return db_user
Delete
@app.delete("/users/{user_id}", status_code=204)
def delete_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
db.delete(user)
db.commit()
return None
Alembic Migrations
# Install alembic
pip install alembic
# Initialize
cd app
alembic init alembic
# Generate migration
alembic revision --autogenerate -m "create users table"
# Apply migration
alembic upgrade head
# Check history
alembic history
Summary
SQLAlchemy provides a powerful ORM for database operations in FastAPI. Combined with Alembic for migrations, it gives you a complete database layer.
Key takeaways:
- Define models with
declarative_base()— classes become tables | - Relationships with
relationship()andForeignKeylink tables | - Session management: create engine, sessionmaker, dependency injection |
- CRUD:
add(),query(),commit(),delete()| - Pagination:
offset()+limit()| - Always hash passwords, never store plain text |
- Alembic handles schema migrations automatically |
- Dependency injection (
Depends(get_db)) provides clean session handling |
What's Next: JWT Authentication
The next chapter covers password hashing and JWT authentication.